diff --git a/lib/galaxy/util/xml_macros.py b/lib/galaxy/util/xml_macros.py
index d126d4950871..beed546463da 100644
--- a/lib/galaxy/util/xml_macros.py
+++ b/lib/galaxy/util/xml_macros.py
@@ -155,7 +155,7 @@ def _expand_tokens_str(s, tokens):
return s
-def _expand_macros(elements, macros, tokens):
+def _expand_macros(elements, macros, tokens, visited=None):
if not macros and not tokens:
return
@@ -164,12 +164,21 @@ def _expand_macros(elements, macros, tokens):
expand_el = element.find('.//expand')
if expand_el is None:
break
- _expand_macro(expand_el, macros, tokens)
+ if visited is None:
+ v = set()
+ else:
+ v = visited
+ _expand_macro(expand_el, macros, tokens, v)
-def _expand_macro(expand_el, macros, tokens):
+def _expand_macro(expand_el, macros, tokens, visited):
macro_name = expand_el.get('macro')
assert macro_name is not None, "Attempted to expand macro with no 'macro' attribute defined."
+
+ # check for cycles in the nested macro expansion
+ assert macro_name not in visited, f"Cycle in nested macros {visited}"
+ visited.add(macro_name)
+
assert macro_name in macros, f"No macro named {macro_name} found, known macros are {', '.join(macros.keys())}."
macro_def = macros[macro_name]
expanded_elements = deepcopy(macro_def.element)
@@ -180,8 +189,9 @@ def _expand_macro(expand_el, macros, tokens):
_expand_tokens(expanded_elements, macro_tokens)
# Recursively expand contained macros.
- _expand_macros(expanded_elements, macros, tokens)
+ _expand_macros(expanded_elements, macros, tokens, visited)
_xml_replace(expand_el, expanded_elements)
+ visited.remove(macro_name)
def _expand_yield_statements(macro_def, expand_el):
diff --git a/test/unit/tool_util/test_tool_loader.py b/test/unit/tool_util/test_tool_loader.py
index a4bbd7fffc66..efa71b772fc7 100644
--- a/test/unit/tool_util/test_tool_loader.py
+++ b/test/unit/tool_util/test_tool_loader.py
@@ -644,3 +644,30 @@ def test_loader_specify_nested_macro_by_token():
'''
+
+
+def test_loader_circular_macros():
+ """
+ check the cycles in nested macros are detected
+ """
+ with TestToolDirectory() as tool_dir:
+ tool_dir.write('''
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+''')
+ try:
+ tool_dir.load()
+ except AssertionError as a:
+ assert str(a) == "Cycle in nested macros {'a', 'b'}"