Skip to content

Commit 8f131eb

Browse files
committed
fix: resolve linked resources against the plain file's directory ENG-205
1 parent 98027a5 commit 8f131eb

5 files changed

Lines changed: 85 additions & 15 deletions

File tree

‎file_utils.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ def open_from(dirs, file_name):
180180
return None
181181

182182

183+
def resolve_linked_resource(dirs, file_name):
184+
"""Return the first existing path for file_name across dirs, or None if not found.
185+
186+
Mirrors the search order used by open_from so that validating a linked resource and
187+
loading it can never disagree.
188+
"""
189+
for dir in dirs:
190+
full_file_name = os.path.join(dir, file_name)
191+
if os.path.exists(full_file_name):
192+
return full_file_name
193+
194+
return None
195+
196+
183197
def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
184198
linked_resources = {}
185199

‎plain_file.py‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def remove_quotes(token):
9090
token.children = tuple(new_children)
9191

9292

93-
def check_section_for_linked_resources(section):
93+
def check_section_for_linked_resources(section, template_dirs):
9494
linked_resources = []
9595
for link in traverse(section, klass=Link):
9696
parsed_url = urlparse(link.node.target)
@@ -99,11 +99,19 @@ def check_section_for_linked_resources(section):
9999
f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
100100
)
101101

102-
if not os.path.exists(link.node.target):
103-
raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} does not exist.")
102+
resolved_target = file_utils.resolve_linked_resource(template_dirs, link.node.target)
103+
if resolved_target is None:
104+
searched_dirs = "\n".join(f" {position}. {dir}" for position, dir in enumerate(template_dirs, start=1))
105+
raise PlainSyntaxError(
106+
f"Plain syntax error: Link {link.node.target} does not exist. "
107+
f"Linked resources are looked up relative to the following directories "
108+
f"(highest to lowest precedence):\n{searched_dirs}"
109+
)
104110

105-
if not os.path.isfile(link.node.target):
106-
raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} must be a file.")
111+
if not os.path.isfile(resolved_target):
112+
raise PlainSyntaxError(
113+
f"Plain syntax error: Link {link.node.target} must be a file (resolved to {resolved_target})."
114+
)
107115

108116
if len(link.node.children) != 1:
109117
raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
@@ -122,15 +130,15 @@ def check_section_for_linked_resources(section):
122130
section.linked_resources = linked_resources
123131

124132

125-
def check_for_linked_resources(plain_source):
133+
def check_for_linked_resources(plain_source, template_dirs):
126134
for specification_heading in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
127135
if specification_heading in plain_source and hasattr(plain_source[specification_heading], "children"):
128136
for requirement in plain_source[specification_heading].children:
129-
check_section_for_linked_resources(requirement)
137+
check_section_for_linked_resources(requirement, template_dirs)
130138

131139
if hasattr(requirement, plain_spec.ACCEPTANCE_TESTS):
132140
for acceptance_test in requirement.acceptance_tests:
133-
check_section_for_linked_resources(acceptance_test)
141+
check_section_for_linked_resources(acceptance_test, template_dirs)
134142

135143

136144
def process_section_code_variables(section, code_variables):
@@ -845,7 +853,7 @@ def plain_file_parser( # noqa: C901
845853

846854
process_acceptance_tests(plain_file_parse_result.plain_source)
847855

848-
check_for_linked_resources(plain_file_parse_result.plain_source)
856+
check_for_linked_resources(plain_file_parse_result.plain_source, template_dirs)
849857

850858
marshalled_plain_source = marshall_plain_source(plain_file_parse_result.plain_source)
851859

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
***definitions***
2+
3+
***implementation reqs***
4+
5+
- Implementation should be written in Python.
6+
7+
***functional specs***
8+
9+
- Links must point at an existing file [missing resource](resources/missing_resource.yaml).

‎tests/data/plainfile/task_manager_with_reference_links.plain‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929

3030
- Implement the entry point for :App:.
3131

32-
- Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](tests/data/plainfile/task_list_ui_specification.yaml).
32+
- Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](task_list_ui_specification.yaml).
3333

34-
- :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](tests/data/plainfile/add_new_task_modal_specification.yaml).
34+
- :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](add_new_task_modal_specification.yaml).
3535

3636
- :User: should be able to delete :Task:.
3737

‎tests/test_plainfileparser.py‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,30 @@ def test_invalid_absolute_link(get_test_data_path):
146146
)
147147

148148

149+
def test_missing_link_error_lists_searched_directories(get_test_data_path):
150+
"""The error tells the user which directories were searched for the linked resource."""
151+
plain_file_dir = get_test_data_path("data/plainfile")
152+
153+
with pytest.raises(PlainSyntaxError) as exception_info:
154+
plain_file.plain_file_parser(
155+
"plain_source_with_missing_link.plain",
156+
[plain_file_dir],
157+
)
158+
159+
message = str(exception_info.value)
160+
assert "resources/missing_resource.yaml does not exist" in message
161+
assert "highest to lowest precedence" in message
162+
assert plain_file_dir in message
163+
164+
149165
def test_reference_link_parsing(get_test_data_path):
150166
_, plain_sections, _ = plain_file.plain_file_parser(
151167
"task_manager_with_reference_links.plain",
152168
[get_test_data_path("data/plainfile")],
153169
)
154170
asserted_resources = [
155-
"tests/data/plainfile/task_list_ui_specification.yaml",
156-
"tests/data/plainfile/add_new_task_modal_specification.yaml",
171+
"task_list_ui_specification.yaml",
172+
"add_new_task_modal_specification.yaml",
157173
]
158174
for functional_requirement in plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS]:
159175
if "linked_resources" not in functional_requirement:
@@ -180,13 +196,36 @@ def test_reference_link_parsing(get_test_data_path):
180196
assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][1]["linked_resources"] == [
181197
{
182198
"text": "task_list_ui_specification.yaml",
183-
"target": "tests/data/plainfile/task_list_ui_specification.yaml",
199+
"target": "task_list_ui_specification.yaml",
200+
}
201+
]
202+
assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][2]["linked_resources"] == [
203+
{
204+
"text": "add_new_task_modal_specification.yaml",
205+
"target": "add_new_task_modal_specification.yaml",
206+
}
207+
]
208+
209+
210+
def test_reference_link_parsing_independent_of_working_directory(get_test_data_path, monkeypatch, tmp_path):
211+
"""Linked resources resolve against the plain file's directory, not the invocation cwd."""
212+
monkeypatch.chdir(tmp_path)
213+
214+
_, plain_sections, _ = plain_file.plain_file_parser(
215+
"task_manager_with_reference_links.plain",
216+
[get_test_data_path("data/plainfile")],
217+
)
218+
219+
assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][1]["linked_resources"] == [
220+
{
221+
"text": "task_list_ui_specification.yaml",
222+
"target": "task_list_ui_specification.yaml",
184223
}
185224
]
186225
assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][2]["linked_resources"] == [
187226
{
188227
"text": "add_new_task_modal_specification.yaml",
189-
"target": "tests/data/plainfile/add_new_task_modal_specification.yaml",
228+
"target": "add_new_task_modal_specification.yaml",
190229
}
191230
]
192231

0 commit comments

Comments
 (0)