diff --git a/lib/BUILD b/lib/BUILD index cff80659..db54cf10 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -23,6 +23,11 @@ bzl_library( srcs = ["dicts.bzl"], ) +bzl_library( + name = "expansion", + srcs = ["expansion.bzl"], +) + bzl_library( name = "modules", srcs = ["modules.bzl"], diff --git a/lib/expansion.bzl b/lib/expansion.bzl new file mode 100644 index 00000000..adb6cab8 --- /dev/null +++ b/lib/expansion.bzl @@ -0,0 +1,909 @@ +"""Skylib module containing functions that aid in environment variable expansion.""" + +_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( + wrapped_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 `wrapped_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: + wrapped_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. Expansion is + done by optionally invoking `wrapped_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 wrapped_expand_location: + expanded_val = wrapped_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_dict_strings_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 `wrapped_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_dict_strings_with_manual_dict_and_location( + wrapped_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 `wrapped_expand_location` and by lookups via `resolution_dict` + dict. + This function does not modify any of the given parameters. + + Args: + wrapped_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()` via `expansion.wrap_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 `wrapped_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( + wrapped_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_dict_strings_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_dict_strings_with_manual_dict( + dict(ctx.var, **additional_lookup_dict), + source_env_dict, + validate_expansion = validate_expansion, + ) + +def _expand_dict_strings_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 `ctx.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. + """ + + additional_lookup_dict = additional_lookup_dict or {} + return _expand_dict_strings_with_manual_dict_and_location( + _wrap_expand_location(ctx, deps), + dict(ctx.var, **additional_lookup_dict), + source_env_dict, + validate_expansion = validate_expansion, + ) + +def _expand_dict_strings_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_dict_strings_with_toolchains( + ctx, + getattr(ctx.attr, env_attr_name), + additional_lookup_dict = additional_lookup_dict, + validate_expansion = validate_expansion, + ) + +def _expand_dict_strings_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_dict_strings_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 _expand_list_strings_with_manual_dict(resolution_dict, source_strings, validate_expansion = False): + """ + Recursively expands all values in `source_strings` using the given lookup data. + + All values of `source_strings` are returned in the resultant list with values expanded by + lookups via `resolution_dict` dict. + Note that the recursion performed is only among `resolution_dict`, as lists are not associative + datatypes (no mapping via `source_strings`). + 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_strings: (Required) The source for all desired expansions. All values will + appear within the returned list, 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 list of strings with all values from `source_strings`, where all values have been + recursively expanded. + """ + expanded_vals = [] + for unexpanded_val in source_strings: + expanded_val = _expand_all_keys_in_str( + None, # No `wrapped_expand_location` available + resolution_dict, + {}, # No `env_replacement_dict` used. + unexpanded_val, + ) + if validate_expansion: + _validate_all_keys_expanded(expanded_val, fail_instead_of_return = True) + expanded_vals.append(expanded_val) + return expanded_vals + +def _expand_list_strings_with_manual_dict_and_location( + wrapped_expand_location, + resolution_dict, + source_strings, + validate_expansion = False): + """ + Recursively expands all values in `source_strings` using the given logic / lookup data. + + All values of `source_strings` are returned in the resultant dict with values expanded by + location expansion logic via `wrapped_expand_location` and by lookups via `resolution_dict` + dict. + Note that the recursion performed is only among `resolution_dict` and + `wrapped_expand_location`, as lists are not associative datatypes (no mapping via + `source_strings`). + This function does not modify any of the given parameters. + + Args: + wrapped_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()` via `expansion.wrap_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_strings: (Required) The source for all desired expansions. All values will + appear within the returned list, with all values fully expanded by the + logic expansion logic of `wrapped_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 list of strings with all values from `source_strings`, where all values have been + recursively expanded. + """ + expanded_vals = [] + for unexpanded_val in source_strings: + expanded_val = _expand_all_keys_in_str( + wrapped_expand_location, + resolution_dict, + {}, # No `env_replacement_dict` used. + unexpanded_val, + ) + if validate_expansion: + _validate_all_keys_expanded(expanded_val, fail_instead_of_return = True) + expanded_vals.append(expanded_val) + return expanded_vals + +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 + +def _wrap_expand_location(ctx, deps): + """ + Returns a function which is a wrapped version of `ctx.expand_location()`. + + Creates a function which takes a single string input parameter and returns that string after + expanding all `$(location ...)` (and similar) substrings. The returned function is backed by + `ctx.expand_location()`, where the given `deps` are used for expansion. + + Args: + ctx: (Required) The bazel context object. This is used to access `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. + + Returns: + A wrapped function which works similar to `ctx.expand_location()`, but takes the input string + as the only argument. + """ + + def _wrapped_expand_location(input_str): + return ctx.expand_location(input_str, deps) + + return _wrapped_expand_location + +expansion = struct( + expand_dict_strings_with_manual_dict = _expand_dict_strings_with_manual_dict, + expand_dict_strings_with_manual_dict_and_location = _expand_dict_strings_with_manual_dict_and_location, + expand_dict_strings_with_toolchains = _expand_dict_strings_with_toolchains, + expand_dict_strings_with_toolchains_attr = _expand_dict_strings_with_toolchains_attr, + expand_dict_strings_with_toolchains_and_location = _expand_dict_strings_with_toolchains_and_location, + expand_dict_strings_with_toolchains_and_location_attr = _expand_dict_strings_with_toolchains_and_location_attr, + expand_list_strings_with_manual_dict = _expand_list_strings_with_manual_dict, + expand_list_strings_with_manual_dict_and_location = _expand_list_strings_with_manual_dict_and_location, + validate_expansions = _validate_expansions, + wrap_expand_location = _wrap_expand_location, +) diff --git a/tests/BUILD b/tests/BUILD index 7d978cb2..8c236ca0 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -3,6 +3,7 @@ load(":build_test_tests.bzl", "build_test_test_suite") load(":collections_tests.bzl", "collections_test_suite") load(":common_settings_tests.bzl", "common_settings_test_suite") load(":dicts_tests.bzl", "dicts_test_suite") +load(":expansion_tests.bzl", "expansion_test_suite") load(":modules_test.bzl", "modules_test_suite") load(":new_sets_tests.bzl", "new_sets_test_suite") load(":partial_tests.bzl", "partial_test_suite") @@ -35,6 +36,8 @@ common_settings_test_suite() dicts_test_suite() +expansion_test_suite() + modules_test_suite() new_sets_test_suite() diff --git a/tests/expansion_tests.bzl b/tests/expansion_tests.bzl new file mode 100644 index 00000000..a21bd391 --- /dev/null +++ b/tests/expansion_tests.bzl @@ -0,0 +1,1189 @@ +"""Unit tests for expansion.bzl.""" + +load("//lib:expansion.bzl", "expansion") +load("//lib:unittest.bzl", "asserts", "unittest") + +# String constants + +_TEST_DEP_TARGET_NAME = "expansion_tests__dummy" + +_MOCK_LOCATION_PATH_OF_DUMMY = "location/path/of/dummy" +_MOCK_EXECPATH_PATH_OF_DUMMY = "execpath/path/of/dummy" +_MOCK_ROOTPATH_PATH_OF_DUMMY = "rootpath/path/of/dummy" +_MOCK_RLOCATIONPATH_PATH_OF_DUMMY = "rlocationpath/path/of/dummy" + +_GENRULE_LOCATION_PATH_OF_DUMMY = "bazel-out/k8-fastbuild/bin/tests/dummy.txt" +_GENRULE_EXECPATH_PATH_OF_DUMMY = "bazel-out/k8-fastbuild/bin/tests/dummy.txt" +_GENRULE_ROOTPATH_PATH_OF_DUMMY = "tests/dummy.txt" +_GENRULE_RLOCATIONPATH_PATH_OF_DUMMY = "_main/tests/dummy.txt" + +_LINUX_FASTBUILD_SUBPATH = "k8-fastbuild" +_MAC_FASTBUILD_SUBPATH = "darwin_x86_64-fastbuild" +_WIN_FASTBUILD_SUBPATH = "x64_windows-fastbuild" + +# Test input dicts + +# buildifier: disable=unsorted-dict-items +_ENV_DICT = { + "SIMPLE_VAL": "hello_world", + "ESCAPED_SIMPLE_VAL": "$$SIMPLE_VAL", + "LOCATION_VAL": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "EXECPATH_VAL": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "ROOTPATH_VAL": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "RLOCATIONPATH_VAL": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_ENV_VAR_RAW": "$TOOLCHAIN_ENV_VAR", + "TOOLCHAIN_ENV_VAR_PAREN": "$(TOOLCHAIN_ENV_VAR)", + "TOOLCHAIN_ENV_VAR_CURLY": "${TOOLCHAIN_ENV_VAR}", + "TOOLCHAIN_ENV_VAR2_RAW": "$TOOLCHAIN_ENV_VAR2", + "TOOLCHAIN_ENV_VAR2_PAREN": "$(TOOLCHAIN_ENV_VAR2)", + "TOOLCHAIN_ENV_VAR2_CURLY": "${TOOLCHAIN_ENV_VAR2}", + "TOOLCHAIN_INDIRECT_ENV_VAR_RAW": "$TOOLCHAIN_INDIRECT_ENV_VAR_RAW", + "TOOLCHAIN_INDIRECT_ENV_VAR_PAREN": "$(TOOLCHAIN_INDIRECT_ENV_VAR_PAREN)", + "TOOLCHAIN_INDIRECT_ENV_VAR_CURLY": "${TOOLCHAIN_INDIRECT_ENV_VAR_CURLY}", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": "$TOOLCHAIN_TO_LOCATION_ENV_VAR", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": "$(TOOLCHAIN_TO_LOCATION_ENV_VAR)", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": "${TOOLCHAIN_TO_LOCATION_ENV_VAR}", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_RAW": "$BACK_TO_ENV_DICT_VAR_RAW", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_PAREN": "$(BACK_TO_ENV_DICT_VAR_PAREN)", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_CURLY": "${BACK_TO_ENV_DICT_VAR_CURLY}", + "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_ENV_VAR_RAW": "$TOOLCHAIN_ENV_VAR_RAW", + "INDIRECT_TOOLCHAIN_ENV_VAR_PAREN": "$(TOOLCHAIN_ENV_VAR_RAW)", + "INDIRECT_TOOLCHAIN_ENV_VAR_CURLY": "${TOOLCHAIN_ENV_VAR_RAW}", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": "$TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": "$(TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW)", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": "${TOOLCHAIN_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_ENV_VAR_RAW-$INDIRECT_TOOLCHAIN_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_ENV_VAR_RAW)-$(INDIRECT_TOOLCHAIN_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_ENV_VAR_RAW}-${INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW}" + ), + "UNRECOGNIZED_VAR": "$NOPE", + "UNRECOGNIZED_FUNC": "$(nope :" + _TEST_DEP_TARGET_NAME + ")", +} + +# buildifier: disable=unsorted-dict-items +_TOOLCHAIN_DICT = { + "TOOLCHAIN_ENV_VAR": "flag_value", + "TOOLCHAIN_ENV_VAR2": "flag_value_2", + "TOOLCHAIN_TO_LOCATION_ENV_VAR": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_INDIRECT_ENV_VAR_RAW": "$TOOLCHAIN_ENV_VAR", + "TOOLCHAIN_INDIRECT_ENV_VAR_PAREN": "$(TOOLCHAIN_ENV_VAR)", + "TOOLCHAIN_INDIRECT_ENV_VAR_CURLY": "${TOOLCHAIN_ENV_VAR}", + "BACK_TO_ENV_DICT_VAR_RAW": "$SIMPLE_VAL", + "BACK_TO_ENV_DICT_VAR_PAREN": "$(SIMPLE_VAL)", + "BACK_TO_ENV_DICT_VAR_CURLY": "${SIMPLE_VAL}", +} + +# Test expected output dicts + +# buildifier: disable=unsorted-dict-items +_EXPECTED_RESOLVED_DICT_NO_LOCATION = { + "SIMPLE_VAL": "hello_world", + "ESCAPED_SIMPLE_VAL": "$$SIMPLE_VAL", + "LOCATION_VAL": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "EXECPATH_VAL": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "ROOTPATH_VAL": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "RLOCATIONPATH_VAL": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_ENV_VAR_RAW": "flag_value", + "TOOLCHAIN_ENV_VAR_PAREN": "flag_value", + "TOOLCHAIN_ENV_VAR_CURLY": "flag_value", + "TOOLCHAIN_ENV_VAR2_RAW": "flag_value_2", + "TOOLCHAIN_ENV_VAR2_PAREN": "flag_value_2", + "TOOLCHAIN_ENV_VAR2_CURLY": "flag_value_2", + "TOOLCHAIN_INDIRECT_ENV_VAR_RAW": "flag_value", + "TOOLCHAIN_INDIRECT_ENV_VAR_PAREN": "flag_value", + "TOOLCHAIN_INDIRECT_ENV_VAR_CURLY": "flag_value", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_RAW": "hello_world", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_PAREN": "hello_world", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_CURLY": "hello_world", + "INDIRECT_SIMPLE_VAL_RAW": "hello_world", + "INDIRECT_SIMPLE_VAL_PAREN": "hello_world", + "INDIRECT_SIMPLE_VAL_CURLY": "hello_world", + "INDIRECT_ESCAPED_SIMPLE_VAL_RAW": "$$SIMPLE_VAL", + "INDIRECT_ESCAPED_SIMPLE_VAL_PAREN": "$$SIMPLE_VAL", + "INDIRECT_ESCAPED_SIMPLE_VAL_CURLY": "$$SIMPLE_VAL", + "INDIRECT_LOCATION_VAL_RAW": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_LOCATION_VAL_PAREN": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_LOCATION_VAL_CURLY": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_EXECPATH_VAL_RAW": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_EXECPATH_VAL_PAREN": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_EXECPATH_VAL_CURLY": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_ROOTPATH_VAL_RAW": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_ROOTPATH_VAL_PAREN": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_ROOTPATH_VAL_CURLY": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_RLOCATIONPATH_VAL_RAW": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_RLOCATIONPATH_VAL_PAREN": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_RLOCATIONPATH_VAL_CURLY": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_TOOLCHAIN_ENV_VAR_RAW": "flag_value", + "INDIRECT_TOOLCHAIN_ENV_VAR_PAREN": "flag_value", + "INDIRECT_TOOLCHAIN_ENV_VAR_CURLY": "flag_value", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "MULTI_INDIRECT_RAW": ( + "hello_world-$$SIMPLE_VAL-$(location :" + + _TEST_DEP_TARGET_NAME + + ")-$(rlocationpath :" + + _TEST_DEP_TARGET_NAME + + ")-flag_value-$(location :" + + _TEST_DEP_TARGET_NAME + + ")" + ), + "MULTI_INDIRECT_PAREN": ( + "hello_world-$$SIMPLE_VAL-$(location :" + + _TEST_DEP_TARGET_NAME + + ")-$(rlocationpath :" + + _TEST_DEP_TARGET_NAME + + ")-flag_value-$(location :" + + _TEST_DEP_TARGET_NAME + + ")" + ), + "MULTI_INDIRECT_CURLY": ( + "hello_world-$$SIMPLE_VAL-$(location :" + + _TEST_DEP_TARGET_NAME + + ")-$(rlocationpath :" + + _TEST_DEP_TARGET_NAME + + ")-flag_value-$(location :" + + _TEST_DEP_TARGET_NAME + + ")" + ), + "UNRECOGNIZED_VAR": "$NOPE", + "UNRECOGNIZED_FUNC": "$(nope :" + _TEST_DEP_TARGET_NAME + ")", +} + +# buildifier: disable=unsorted-dict-items +_EXPECTED_RESOLVED_DICT_WITH_MOCKED_LOCATION = dict(_EXPECTED_RESOLVED_DICT_NO_LOCATION, **{ + "LOCATION_VAL": _MOCK_LOCATION_PATH_OF_DUMMY, + "EXECPATH_VAL": _MOCK_EXECPATH_PATH_OF_DUMMY, + "ROOTPATH_VAL": _MOCK_ROOTPATH_PATH_OF_DUMMY, + "RLOCATIONPATH_VAL": _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": _MOCK_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": _MOCK_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_RAW": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_PAREN": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_CURLY": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_RAW": _MOCK_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_PAREN": _MOCK_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_CURLY": _MOCK_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_RAW": _MOCK_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_PAREN": _MOCK_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_CURLY": _MOCK_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_RAW": _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_PAREN": _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_CURLY": _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": _MOCK_LOCATION_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": _MOCK_LOCATION_PATH_OF_DUMMY, + "MULTI_INDIRECT_RAW": ( + "hello_world-$$SIMPLE_VAL-" + + _MOCK_LOCATION_PATH_OF_DUMMY + + "-" + + _MOCK_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _MOCK_LOCATION_PATH_OF_DUMMY + ), + "MULTI_INDIRECT_PAREN": ( + "hello_world-$$SIMPLE_VAL-" + + _MOCK_LOCATION_PATH_OF_DUMMY + + "-" + + _MOCK_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _MOCK_LOCATION_PATH_OF_DUMMY + ), + "MULTI_INDIRECT_CURLY": ( + "hello_world-$$SIMPLE_VAL-" + + _MOCK_LOCATION_PATH_OF_DUMMY + + "-" + + _MOCK_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _MOCK_LOCATION_PATH_OF_DUMMY + ), +}) + +# buildifier: disable=unsorted-dict-items +_EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION = dict(_EXPECTED_RESOLVED_DICT_NO_LOCATION, **{ + "LOCATION_VAL": _GENRULE_LOCATION_PATH_OF_DUMMY, + "EXECPATH_VAL": _GENRULE_EXECPATH_PATH_OF_DUMMY, + "ROOTPATH_VAL": _GENRULE_ROOTPATH_PATH_OF_DUMMY, + "RLOCATIONPATH_VAL": _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": _GENRULE_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": _GENRULE_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_RAW": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_PAREN": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_LOCATION_VAL_CURLY": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_RAW": _GENRULE_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_PAREN": _GENRULE_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_EXECPATH_VAL_CURLY": _GENRULE_EXECPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_RAW": _GENRULE_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_PAREN": _GENRULE_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_ROOTPATH_VAL_CURLY": _GENRULE_ROOTPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_RAW": _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_PAREN": _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_RLOCATIONPATH_VAL_CURLY": _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": _GENRULE_LOCATION_PATH_OF_DUMMY, + "INDIRECT_TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": _GENRULE_LOCATION_PATH_OF_DUMMY, + "MULTI_INDIRECT_RAW": ( + "hello_world-$$SIMPLE_VAL-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + + "-" + + _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + ), + "MULTI_INDIRECT_PAREN": ( + "hello_world-$$SIMPLE_VAL-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + + "-" + + _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + ), + "MULTI_INDIRECT_CURLY": ( + "hello_world-$$SIMPLE_VAL-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + + "-" + + _GENRULE_RLOCATIONPATH_PATH_OF_DUMMY + + "-flag_value-" + + _GENRULE_LOCATION_PATH_OF_DUMMY + ), +}) + +# "No recursion" dicts leave most ("TO_ENV_DICT" and "INDIRECT") entries unexpanded. +# buildifier: disable=unsorted-dict-items +_EXPECTED_RESOLVED_DICT_NO_LOCATION_NO_RECURSION = dict(_ENV_DICT, **{ + "SIMPLE_VAL": "hello_world", + "ESCAPED_SIMPLE_VAL": "$$SIMPLE_VAL", + "LOCATION_VAL": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "EXECPATH_VAL": "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + "ROOTPATH_VAL": "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + "RLOCATIONPATH_VAL": "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_ENV_VAR_RAW": "flag_value", + "TOOLCHAIN_ENV_VAR_PAREN": "flag_value", + "TOOLCHAIN_ENV_VAR_CURLY": "flag_value", + "TOOLCHAIN_ENV_VAR2_RAW": "flag_value_2", + "TOOLCHAIN_ENV_VAR2_PAREN": "flag_value_2", + "TOOLCHAIN_ENV_VAR2_CURLY": "flag_value_2", + "TOOLCHAIN_INDIRECT_ENV_VAR_RAW": "flag_value", + "TOOLCHAIN_INDIRECT_ENV_VAR_PAREN": "flag_value", + "TOOLCHAIN_INDIRECT_ENV_VAR_CURLY": "flag_value", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": "$(location :" + _TEST_DEP_TARGET_NAME + ")", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_RAW": "$SIMPLE_VAL", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_PAREN": "$(SIMPLE_VAL)", + "TOOLCHAIN_TO_ENV_DICT_ENV_VAR_CURLY": "${SIMPLE_VAL}", +}) + +# buildifier: disable=unsorted-dict-items +_EXPECTED_RESOLVED_DICT_WITH_MOCKED_LOCATION_NO_RECURSION = dict(_EXPECTED_RESOLVED_DICT_NO_LOCATION_NO_RECURSION, **{ + "LOCATION_VAL": _MOCK_LOCATION_PATH_OF_DUMMY, + "EXECPATH_VAL": _MOCK_EXECPATH_PATH_OF_DUMMY, + "ROOTPATH_VAL": _MOCK_ROOTPATH_PATH_OF_DUMMY, + "RLOCATIONPATH_VAL": _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_RAW": _MOCK_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_PAREN": _MOCK_LOCATION_PATH_OF_DUMMY, + "TOOLCHAIN_TO_LOCATION_ENV_VAR_CURLY": _MOCK_LOCATION_PATH_OF_DUMMY, +}) + +# Unresolved/unterminated validation test input values and expected values + +_UNRESOLVED_SUBSTRINGS = [ + "$SOME_UNEXPANDED_VAR_RAW", + "$(SOME_UNEXPANDED_VAR_PAREN)", + "${SOME_UNEXPANDED_VAR_CURLY}", +] + +_UNRESOLVED_SUBSTRINGS_FORMAT_STRINGS = [ + "{}", + "prefix-{}", + "prefix {}", + "{}-suffix", + "{} suffix", + "prefix-{}-suffix", + "prefix {} suffix", +] + +_UNRESOLVED_SUBSTRING_EXPECTED_ERROR_MESSAGES = [ + "Unexpanded expression ('{}' in '{}').".format(unresolved_substr, full_string) + for unresolved_substr in _UNRESOLVED_SUBSTRINGS + for full_string in [ + format_str.format(unresolved_substr) + for format_str in _UNRESOLVED_SUBSTRINGS_FORMAT_STRINGS + ] +] + +_TWO_UNRESOLVED_SUBSTRINGS = [ + "{}-{}".format(a, b) + for a in _UNRESOLVED_SUBSTRINGS + for b in _UNRESOLVED_SUBSTRINGS +] + [ + "{} {}".format(a, b) + for a in _UNRESOLVED_SUBSTRINGS + for b in _UNRESOLVED_SUBSTRINGS +] + +_TWO_UNRESOLVED_SUBSTRING_EXPECTED_ERROR_MESSAGES = [ + "Unexpanded expression ('{}' in '{}').".format(unresolved_substr, full_string) + for unresolved_substr in _UNRESOLVED_SUBSTRINGS + for full_string in _TWO_UNRESOLVED_SUBSTRINGS + if unresolved_substr in full_string +] + +_UNRESOLVED_NAMELESS_SUBSTRINGS_FORMAT_STRINGS = [ + "{}-suffix", + "{} suffix", + "prefix-{}-suffix", + "prefix {} suffix", +] + +_UNRESOLVED_NAMELESS_SUBSTRING_EXPECTED_ERROR_MESSAGES = [ + "Unexpanded expression ('{}' in '{}').".format("$", full_string) + for unresolved_substr in _UNRESOLVED_SUBSTRINGS + for full_string in [ + format_str.format("$") + for format_str in _UNRESOLVED_NAMELESS_SUBSTRINGS_FORMAT_STRINGS + ] +] + +_UNTERMINATED_SUBSTRINGS_PAREN = [ + "$(", + "$(VAR", + "$(VAR ", + "$(VAR VAL", +] + +_UNTERMINATED_SUBSTRINGS_CURLY = [ + "${", + "${VAR", + "${VAR ", + "${VAR VAL", +] + +_UNTERMINATED_SUBSTRINGS_FORMAT_STRINGS = [ + "{}", + "prefix-{}", + "prefix {}", +] + +_UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_RAW = [ + "Unterminated '$' expression in '{}'.".format(full_string) + for full_string in [ + format_str.format("$") + for format_str in _UNTERMINATED_SUBSTRINGS_FORMAT_STRINGS + ] +] + +_UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_PAREN = [ + "Unterminated '$(...)' expression ('{}' in '{}').".format(unresolved_substr, full_string) + for unresolved_substr in _UNTERMINATED_SUBSTRINGS_PAREN + for full_string in [ + format_str.format(unresolved_substr) + for format_str in _UNTERMINATED_SUBSTRINGS_FORMAT_STRINGS + ] +] + +_UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_CURLY = [ + "Unterminated '${{...}}' expression ('{}' in '{}').".format(unresolved_substr, full_string) + for unresolved_substr in _UNTERMINATED_SUBSTRINGS_CURLY + for full_string in [ + format_str.format(unresolved_substr) + for format_str in _UNTERMINATED_SUBSTRINGS_FORMAT_STRINGS + ] +] + +# Test helper functions and rules + +def _test_toolchain_impl(ctx): + _ignore = [ctx] # @unused + return [platform_common.TemplateVariableInfo(_TOOLCHAIN_DICT)] + +_test_toolchain = rule( + implementation = _test_toolchain_impl, +) + +def _mock_wrapped_expand_location(input_str): + return input_str.replace( + "$(location :" + _TEST_DEP_TARGET_NAME + ")", + _MOCK_LOCATION_PATH_OF_DUMMY, + ).replace( + "$(execpath :" + _TEST_DEP_TARGET_NAME + ")", + _MOCK_EXECPATH_PATH_OF_DUMMY, + ).replace( + "$(rootpath :" + _TEST_DEP_TARGET_NAME + ")", + _MOCK_ROOTPATH_PATH_OF_DUMMY, + ).replace( + "$(rlocationpath :" + _TEST_DEP_TARGET_NAME + ")", + _MOCK_RLOCATIONPATH_PATH_OF_DUMMY, + ) + +def _fix_platform_dependent_path_for_assertions(platform_dependent_val): + return platform_dependent_val.replace( + _MAC_FASTBUILD_SUBPATH, + _LINUX_FASTBUILD_SUBPATH, + ).replace( + _WIN_FASTBUILD_SUBPATH, + _LINUX_FASTBUILD_SUBPATH, + ) + +# Test cases + +def _expand_dict_strings_with_manual_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_manual_dict()`""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(_TOOLCHAIN_DICT) + + resolved_dict = expansion.expand_dict_strings_with_manual_dict(_TOOLCHAIN_DICT, _ENV_DICT) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, _TOOLCHAIN_DICT) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = _EXPECTED_RESOLVED_DICT_NO_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_manual_dict_test = unittest.make(_expand_dict_strings_with_manual_dict_test_impl) + +def _expand_dict_strings_with_manual_dict_and_location_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_manual_dict_and_location()`""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(_TOOLCHAIN_DICT) + + resolved_dict = expansion.expand_dict_strings_with_manual_dict_and_location( + _mock_wrapped_expand_location, + _TOOLCHAIN_DICT, + _ENV_DICT, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, _TOOLCHAIN_DICT) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = _EXPECTED_RESOLVED_DICT_WITH_MOCKED_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_manual_dict_and_location_test = unittest.make( + _expand_dict_strings_with_manual_dict_and_location_test_impl, +) + +def _expand_dict_strings_with_toolchains_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains()` without extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(env.ctx.var) + + resolved_dict = expansion.expand_dict_strings_with_toolchains(env.ctx, _ENV_DICT) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = _EXPECTED_RESOLVED_DICT_NO_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_test = unittest.make(_expand_dict_strings_with_toolchains_test_impl) + +def _expand_dict_strings_with_toolchains_with_additional_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains()` with extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(env.ctx.var) + + # buildifier: disable=unsorted-dict-items + additional_lookup_dict = { + "TOOLCHAIN_ENV_VAR2": "expanded from additional dict instead", + "NOPE": "naw, it's fine now.", + } + + resolved_dict = expansion.expand_dict_strings_with_toolchains( + env.ctx, + _ENV_DICT, + additional_lookup_dict = additional_lookup_dict, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # buildifier: disable=unsorted-dict-items + updated_expected_dict = dict(_EXPECTED_RESOLVED_DICT_NO_LOCATION, **{ + "TOOLCHAIN_ENV_VAR2_RAW": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_PAREN": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_CURLY": "expanded from additional dict instead", + "UNRECOGNIZED_VAR": "naw, it's fine now.", + }) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = updated_expected_dict[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_with_additional_dict_test = unittest.make( + _expand_dict_strings_with_toolchains_with_additional_dict_test_impl, +) + +def _expand_dict_strings_with_toolchains_attr_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_attr()` without extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(env.ctx.attr.env) + toolchain_dict_copy = dict(env.ctx.var) + + resolved_dict = expansion.expand_dict_strings_with_toolchains_attr(env.ctx) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, env.ctx.attr.env) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, env.ctx.attr.env.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in env.ctx.attr.env.items(): + expected_val = _EXPECTED_RESOLVED_DICT_NO_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_attr_test = unittest.make( + _expand_dict_strings_with_toolchains_attr_test_impl, + attrs = { + "env": attr.string_dict(), + }, +) + +def _expand_dict_strings_with_toolchains_attr_with_additional_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_attr()` with extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(env.ctx.attr.env) + toolchain_dict_copy = dict(env.ctx.var) + + # buildifier: disable=unsorted-dict-items + additional_lookup_dict = { + "TOOLCHAIN_ENV_VAR2": "expanded from additional dict instead", + "NOPE": "naw, it's fine now.", + } + + resolved_dict = expansion.expand_dict_strings_with_toolchains_attr( + env.ctx, + additional_lookup_dict = additional_lookup_dict, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, env.ctx.attr.env) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, env.ctx.attr.env.keys(), resolved_dict.keys()) + + # buildifier: disable=unsorted-dict-items + updated_expected_dict = dict(_EXPECTED_RESOLVED_DICT_NO_LOCATION, **{ + "TOOLCHAIN_ENV_VAR2_RAW": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_PAREN": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_CURLY": "expanded from additional dict instead", + "UNRECOGNIZED_VAR": "naw, it's fine now.", + }) + + # Check all output resolved values against expected resolved values. + for env_key, _ in env.ctx.attr.env.items(): + expected_val = updated_expected_dict[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_attr_with_additional_dict_test = unittest.make( + _expand_dict_strings_with_toolchains_attr_with_additional_dict_test_impl, + attrs = { + "env": attr.string_dict(), + }, +) + +def _expand_dict_strings_with_toolchains_and_location_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_and_location()` without extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(env.ctx.var) + + resolved_dict = expansion.expand_dict_strings_with_toolchains_and_location( + env.ctx, + [ctx.attr.target], + _ENV_DICT, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = _EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_and_location_test = unittest.make( + _expand_dict_strings_with_toolchains_and_location_test_impl, + attrs = { + "target": attr.label(), + }, +) + +def _expand_dict_strings_with_toolchains_and_location_with_additional_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_and_location()` with extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(_ENV_DICT) + toolchain_dict_copy = dict(env.ctx.var) + + # buildifier: disable=unsorted-dict-items + additional_lookup_dict = { + "TOOLCHAIN_ENV_VAR2": "expanded from additional dict instead", + "NOPE": "naw, it's fine now.", + } + + resolved_dict = expansion.expand_dict_strings_with_toolchains_and_location( + env.ctx, + [ctx.attr.target], + _ENV_DICT, + additional_lookup_dict = additional_lookup_dict, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, _ENV_DICT) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, _ENV_DICT.keys(), resolved_dict.keys()) + + # buildifier: disable=unsorted-dict-items + updated_expected_dict = dict(_EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION, **{ + "TOOLCHAIN_ENV_VAR2_RAW": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_PAREN": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_CURLY": "expanded from additional dict instead", + "UNRECOGNIZED_VAR": "naw, it's fine now.", + }) + + # Check all output resolved values against expected resolved values. + for env_key, _ in _ENV_DICT.items(): + expected_val = updated_expected_dict[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_and_location_with_additional_dict_test = unittest.make( + _expand_dict_strings_with_toolchains_and_location_with_additional_dict_test_impl, + attrs = { + "target": attr.label(), + }, +) + +def _expand_dict_strings_with_toolchains_and_location_attr_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_and_location_attr()` without extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(env.ctx.attr.env) + toolchain_dict_copy = dict(env.ctx.var) + + resolved_dict = expansion.expand_dict_strings_with_toolchains_and_location_attr(env.ctx) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, env.ctx.attr.env) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, env.ctx.attr.env.keys(), resolved_dict.keys()) + + # Check all output resolved values against expected resolved values. + for env_key, _ in env.ctx.attr.env.items(): + expected_val = _EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_and_location_attr_test = unittest.make( + _expand_dict_strings_with_toolchains_and_location_attr_test_impl, + attrs = { + "deps": attr.label_list(), + "env": attr.string_dict(), + }, +) + +def _expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_toolchains_and_location_attr()` with extra dict""" + env = unittest.begin(ctx) + + env_dict_copy = dict(env.ctx.attr.env) + toolchain_dict_copy = dict(env.ctx.var) + + # buildifier: disable=unsorted-dict-items + additional_lookup_dict = { + "TOOLCHAIN_ENV_VAR2": "expanded from additional dict instead", + "NOPE": "naw, it's fine now.", + } + + resolved_dict = expansion.expand_dict_strings_with_toolchains_and_location_attr( + env.ctx, + additional_lookup_dict = additional_lookup_dict, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_copy, env.ctx.attr.env) + asserts.equals(env, toolchain_dict_copy, env.ctx.var) + + # Check that the output has exact same key set as original input. + asserts.equals(env, env.ctx.attr.env.keys(), resolved_dict.keys()) + + # buildifier: disable=unsorted-dict-items + updated_expected_dict = dict(_EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION, **{ + "TOOLCHAIN_ENV_VAR2_RAW": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_PAREN": "expanded from additional dict instead", + "TOOLCHAIN_ENV_VAR2_CURLY": "expanded from additional dict instead", + "UNRECOGNIZED_VAR": "naw, it's fine now.", + }) + + # Check all output resolved values against expected resolved values. + for env_key, _ in env.ctx.attr.env.items(): + expected_val = updated_expected_dict[env_key] + resolved_val = resolved_dict[env_key] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test = unittest.make( + _expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test_impl, + attrs = { + "deps": attr.label_list(), + "env": attr.string_dict(), + }, +) + +def _expand_list_strings_with_manual_dict_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_manual_dict()`""" + env = unittest.begin(ctx) + + env_dict_values = _ENV_DICT.values() + env_dict_values_copy = list(env_dict_values) + toolchain_dict_copy = dict(_TOOLCHAIN_DICT) + + resolved_list = expansion.expand_list_strings_with_manual_dict(_TOOLCHAIN_DICT, env_dict_values) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_values_copy, env_dict_values) + asserts.equals(env, toolchain_dict_copy, _TOOLCHAIN_DICT) + + # Check that the output has exact same length as original input. + asserts.equals(env, len(env_dict_values), len(resolved_list)) + + # Check all output resolved values against expected resolved values. + for env_key, env_orig_val in _ENV_DICT.items(): + index_in_unexpanded = env_dict_values.index(env_orig_val) + expected_val = _EXPECTED_RESOLVED_DICT_NO_LOCATION_NO_RECURSION[env_key] + resolved_val = resolved_list[index_in_unexpanded] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_list_strings_with_manual_dict_test = unittest.make(_expand_list_strings_with_manual_dict_test_impl) + +def _expand_list_strings_with_manual_dict_and_location_test_impl(ctx): + """Test `expansion.expand_dict_strings_with_manual_dict_and_location()`""" + env = unittest.begin(ctx) + + env_dict_values = _ENV_DICT.values() + env_dict_values_copy = list(env_dict_values) + toolchain_dict_copy = dict(_TOOLCHAIN_DICT) + + resolved_list = expansion.expand_list_strings_with_manual_dict_and_location( + _mock_wrapped_expand_location, + _TOOLCHAIN_DICT, + env_dict_values, + ) + + # Check that the inputs are not mutated. + asserts.equals(env, env_dict_values_copy, env_dict_values) + asserts.equals(env, toolchain_dict_copy, _TOOLCHAIN_DICT) + + # Check that the output has exact same length as original input. + asserts.equals(env, len(env_dict_values), len(resolved_list)) + + # Check all output resolved values against expected resolved values. + for env_key, env_orig_val in _ENV_DICT.items(): + index_in_unexpanded = env_dict_values.index(env_orig_val) + expected_val = _EXPECTED_RESOLVED_DICT_WITH_MOCKED_LOCATION_NO_RECURSION[env_key] + resolved_val = resolved_list[index_in_unexpanded] + resolved_val = _fix_platform_dependent_path_for_assertions(resolved_val) + + # Replace `expected` with proper value given bazel version (may differ for rlocation path). + if "_main" in expected_val and "_main" not in resolved_val: + expected_val = expected_val.replace("_main", "bazel_skylib") + asserts.equals(env, expected_val, resolved_val) + + return unittest.end(env) + +_expand_list_strings_with_manual_dict_and_location_test = unittest.make( + _expand_list_strings_with_manual_dict_and_location_test_impl, +) + +def _validate_expansions_on_fully_resolved_values_test_impl(ctx): + """Test `expansion.validate_expansions()` with fully resolved strings""" + env = unittest.begin(ctx) + + # Remove the values that are meant to be unexpanded in other tests. + _values_with_mocked_location = dict(_EXPECTED_RESOLVED_DICT_WITH_MOCKED_LOCATION) + _values_with_mocked_location.pop("UNRECOGNIZED_VAR") + _values_with_mocked_location.pop("UNRECOGNIZED_FUNC") + + _values_with_genrule_location = dict(_EXPECTED_RESOLVED_DICT_WITH_GENRULE_LOCATION) + _values_with_genrule_location.pop("UNRECOGNIZED_VAR") + _values_with_genrule_location.pop("UNRECOGNIZED_FUNC") + + failure_messages = [] + for fail_on_unexpanded in (True, False): + failure_messages += expansion.validate_expansions( + _values_with_mocked_location.values(), + fail_instead_of_return = fail_on_unexpanded, + ) + failure_messages += expansion.validate_expansions( + _values_with_genrule_location.values(), + fail_instead_of_return = fail_on_unexpanded, + ) + + failure_messages += expansion.validate_expansions( + [ + "$$SOME_ESCAPED_VAR", + "prefix-$$SOME_ESCAPED_VAR", + "$$SOME_ESCAPED_VAR-suffix", + "prefix-$$SOME_ESCAPED_VAR-suffix", + "$$", + "prefix-$$", + "$$-suffix", + "prefix-$$-suffix", + ], + fail_instead_of_return = fail_on_unexpanded, + ) + asserts.equals(env, [], failure_messages) + + return unittest.end(env) + +_validate_expansions_on_fully_resolved_values_test = unittest.make( + _validate_expansions_on_fully_resolved_values_test_impl, +) + +def _validate_expansions_on_unresolved_or_unterminated_values_test_impl(ctx): + """ + Test `expansion.validate_expansions()` with unresolved or unterminated values + + Will be used with multiple targets for specific unresolved/unterminated values. + """ + env = unittest.begin(ctx) + + all_error_messages = [] + for bad_substring in env.ctx.attr.bad_substrings: + for string_format in env.ctx.attr.string_formats: + bad_str = string_format.format(bad_substring) + + # Get out error messages (instead of calling `fail()` internally). + # This allows us to parameterize the test in this impl, instead of parameterizing the + # test target definitions. + # Also allows using `unittest` without `analysistest`. + error_messages = expansion.validate_expansions( + [bad_str], + fail_instead_of_return = False, + ) + + expected_error_messages_per_call = ctx.attr.expected_error_messages_per_call + asserts.true( + env, + len(error_messages) == expected_error_messages_per_call, + "Wrong error message size. Expected count: {}.\n Got messages:\n{}".format( + expected_error_messages_per_call, + "\n".join(error_messages), + ), + ) + all_error_messages.extend(error_messages) + + for expected_failure in ctx.attr.expected_failures: + asserts.true( + env, + expected_failure in all_error_messages, + "'{}' not in:\n{}".format(expected_failure, "\n".join(all_error_messages)), + ) + + return unittest.end(env) + +_validate_expansions_on_unresolved_or_unterminated_values_test = unittest.make( + _validate_expansions_on_unresolved_or_unterminated_values_test_impl, + attrs = { + "bad_substrings": attr.string_list( + mandatory = True, + allow_empty = False, + ), + "expected_error_messages_per_call": attr.int( + default = 1, + ), + "expected_failures": attr.string_list( + mandatory = True, + allow_empty = False, + ), + "string_formats": attr.string_list( + mandatory = True, + allow_empty = False, + ), + }, +) + +# buildifier: disable=unnamed-macro +def expansion_test_suite(): + """Creates the test targets and test suite for expansion.bzl tests.""" + + native.genrule( + name = _TEST_DEP_TARGET_NAME, + outs = ["dummy.txt"], + cmd = "touch $@", + ) + + _test_toolchain( + name = "expansion_tests__test_toolchain", + ) + + _expand_dict_strings_with_manual_dict_test( + name = "expansion_tests__expand_dict_strings_with_manual_dict_test", + ) + _expand_dict_strings_with_manual_dict_and_location_test( + name = "expansion_tests__expand_dict_strings_with_manual_dict_and_location_test", + ) + _expand_dict_strings_with_toolchains_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_test", + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_with_additional_dict_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_with_additional_dict_test", + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_attr_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_attr_test", + env = _ENV_DICT, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_attr_with_additional_dict_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_attr_with_additional_dict_test", + env = _ENV_DICT, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_and_location_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_and_location_test", + target = ":" + _TEST_DEP_TARGET_NAME, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_and_location_with_additional_dict_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_and_location_with_additional_dict_test", + target = ":" + _TEST_DEP_TARGET_NAME, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_and_location_attr_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_and_location_attr_test", + deps = [":" + _TEST_DEP_TARGET_NAME], + env = _ENV_DICT, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test( + name = "expansion_tests__expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test", + deps = [":" + _TEST_DEP_TARGET_NAME], + env = _ENV_DICT, + toolchains = [":expansion_tests__test_toolchain"], + ) + _expand_list_strings_with_manual_dict_test( + name = "expansion_tests__expand_list_strings_with_manual_dict_test", + ) + _expand_list_strings_with_manual_dict_and_location_test( + name = "expansion_tests__expand_list_strings_with_manual_dict_and_location_test", + ) + _validate_expansions_on_fully_resolved_values_test( + name = "expansion_tests__validate_expansions_on_fully_resolved_values_test", + ) + _validate_expansions_on_unresolved_or_unterminated_values_test( + name = "expansion_tests__validate_expansions_on_unresolved_values_test", + bad_substrings = _UNRESOLVED_SUBSTRINGS, + expected_failures = _UNRESOLVED_SUBSTRING_EXPECTED_ERROR_MESSAGES, + string_formats = _UNRESOLVED_SUBSTRINGS_FORMAT_STRINGS, + ) + _validate_expansions_on_unresolved_or_unterminated_values_test( + name = "expansion_tests__validate_expansions_on_two_unresolved_values_test", + bad_substrings = _TWO_UNRESOLVED_SUBSTRINGS, + expected_error_messages_per_call = 2, + expected_failures = _TWO_UNRESOLVED_SUBSTRING_EXPECTED_ERROR_MESSAGES, + string_formats = ["{}"], + ) + _validate_expansions_on_unresolved_or_unterminated_values_test( + name = "expansion_tests__validate_expansions_on_nameless_var_values_test", + bad_substrings = ["$"], + expected_failures = _UNRESOLVED_NAMELESS_SUBSTRING_EXPECTED_ERROR_MESSAGES, + string_formats = _UNRESOLVED_NAMELESS_SUBSTRINGS_FORMAT_STRINGS, + ) + _validate_expansions_on_unresolved_or_unterminated_values_test( + name = "expansion_tests__validate_expansions_on_unterminated_values_test", + bad_substrings = ["$"] + _UNTERMINATED_SUBSTRINGS_PAREN + _UNTERMINATED_SUBSTRINGS_CURLY, + expected_failures = ( + _UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_RAW + + _UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_PAREN + + _UNTERMINATED_SUBSTRING_EXPECTED_ERROR_MESSAGES_CURLY + ), + string_formats = _UNTERMINATED_SUBSTRINGS_FORMAT_STRINGS, + ) + + native.test_suite( + name = "expansion_tests", + tests = [ + ":expansion_tests__expand_dict_strings_with_manual_dict_test", + ":expansion_tests__expand_dict_strings_with_manual_dict_and_location_test", + ":expansion_tests__expand_dict_strings_with_toolchains_test", + ":expansion_tests__expand_dict_strings_with_toolchains_with_additional_dict_test", + ":expansion_tests__expand_dict_strings_with_toolchains_attr_test", + ":expansion_tests__expand_dict_strings_with_toolchains_attr_with_additional_dict_test", + ":expansion_tests__expand_dict_strings_with_toolchains_and_location_test", + ":expansion_tests__expand_dict_strings_with_toolchains_and_location_with_additional_dict_test", + ":expansion_tests__expand_dict_strings_with_toolchains_and_location_attr_test", + ":expansion_tests__expand_dict_strings_with_toolchains_and_location_attr_with_additional_dict_test", + ":expansion_tests__expand_list_strings_with_manual_dict_test", + ":expansion_tests__expand_list_strings_with_manual_dict_and_location_test", + ":expansion_tests__validate_expansions_on_fully_resolved_values_test", + ":expansion_tests__validate_expansions_on_unresolved_values_test", + ":expansion_tests__validate_expansions_on_two_unresolved_values_test", + ":expansion_tests__validate_expansions_on_nameless_var_values_test", + ":expansion_tests__validate_expansions_on_unterminated_values_test", + ], + )