StrictDoc Documentation
tests/unit/strictdoc/backend/sdoc_source_code/readers/test_reader_rust.py
Source file coverage
Path:
tests/unit/strictdoc/backend/sdoc_source_code/readers/test_reader_rust.py
Lines:
269
Non-empty lines:
213
Non-empty lines covered with requirements:
213 / 213 (100.0%)
Functions:
27
Functions covered by requirements:
27 / 27 (100.0%)
1
"""
2
@relation(SDOC-SRS-142, scope=file)
3
"""
4
 
5
from pathlib import Path
6
 
7
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
8
    SourceFileTraceabilityInfo,
9
)
10
from strictdoc.backend.sdoc_source_code.reader_rust import (
11
    SourceFileTraceabilityReader_Rust,
12
    rust_canonical_crate_segments,
13
    rust_crate_root_and_name,
14
    rust_module_segments_within_crate,
15
)
16
 
17
 
18
def _read(source: bytes, file_path: str) -> SourceFileTraceabilityInfo:
19
    return SourceFileTraceabilityReader_Rust().read(source, file_path=file_path)
20
 
21
 
22
def _function_named(info: SourceFileTraceabilityInfo, name: str):
23
    matches = [function for function in info.functions if function.name == name]
24
    assert len(matches) == 1, (
25
        f"expected exactly one function named {name!r}, got "
26
        f"{[function.name for function in info.functions]}"
27
    )
28
    return matches[0]
29
 
30
 
31
def _crate(root: Path, package_name: str) -> Path:
32
    root.mkdir(parents=True, exist_ok=True)
33
    (root / "Cargo.toml").write_text(
34
        f'[package]\nname = "{package_name}"\n'
35
        'version = "0.1.0"\nedition = "2024"\n',
36
        encoding="utf-8",
37
    )
38
    return root
39
 
40
 
41
def _read_one(file_path: str, source_body: bytes, fn_name: str):
42
    source = b"/// @relation(REQ-1, scope=function)\n" + source_body
43
    info = _read(source, file_path=file_path)
44
    return _function_named(info, fn_name)
45
 
46
 
47
def test_module_segments_within_crate_mapping():
48
    f = rust_module_segments_within_crate
49
    assert f(()) == []
50
    assert f(("src", "lib.rs")) == []
51
    assert f(("src", "main.rs")) == []
52
    assert f(("src", "model.rs")) == ["model"]
53
    assert f(("src", "model", "mod.rs")) == ["model"]
54
    assert f(("src", "a", "b", "c.rs")) == ["a", "b", "c"]
55
    assert f(("src", "model", "tests.rs")) == ["model", "tests"]
56
    assert f(("tests", "it.rs")) == ["it"]
57
    assert f(("tests", "it", "helper.rs")) == ["it", "helper"]
58
    assert f(("tests", "it", "main.rs")) == ["it"]
59
 
60
 
61
def test_crate_root_and_name_finds_enclosing_package(tmp_path):
62
    _crate(tmp_path, "my_crate")
63
    found = rust_crate_root_and_name(str(tmp_path / "src"))
64
    assert found == (str(tmp_path), "my_crate")
65
 
66
 
67
def test_crate_root_and_name_keeps_hyphenated_package_name(tmp_path):
68
    _crate(tmp_path, "weird-pkg-name")
69
    found = rust_crate_root_and_name(str(tmp_path / "src"))
70
    assert found == (str(tmp_path), "weird-pkg-name")
71
 
72
 
73
def test_crate_root_and_name_skips_workspace_only_manifest(tmp_path):
74
    (tmp_path / "Cargo.toml").write_text(
75
        '[workspace]\nmembers = ["crates/foo"]\n', encoding="utf-8"
76
    )
77
    assert rust_crate_root_and_name(str(tmp_path / "src")) is None
78
 
79
 
80
def test_crate_root_and_name_returns_none_without_manifest(tmp_path):
81
    assert rust_crate_root_and_name(str(tmp_path / "src")) is None
82
 
83
 
84
def test_canonical_segments_crate_qualified(tmp_path):
85
    _crate(tmp_path, "my_crate")
86
    assert rust_canonical_crate_segments(str(tmp_path / "src" / "lib.rs")) == [
87
        "my_crate"
88
    ]
89
    assert rust_canonical_crate_segments(
90
        str(tmp_path / "src" / "a" / "b" / "c.rs")
91
    ) == ["my_crate", "a", "b", "c"]
92
 
93
 
94
def test_canonical_segments_fall_back_to_stem_without_cargo(tmp_path):
95
    flat = tmp_path / "forward_relations.rs"
96
    assert rust_canonical_crate_segments(str(flat)) == ["forward_relations"]
97
 
98
 
99
def test_canonical_segments_none_path():
100
    assert rust_canonical_crate_segments(None) == []
101
 
102
 
103
def test_empty_file():
104
    info = _read(b"", file_path="src/lib.rs")
105
    assert isinstance(info, SourceFileTraceabilityInfo)
106
    assert len(info.markers) == 0
107
 
108
 
109
def test_relation_marker_is_attached_to_function(tmp_path):
110
    _crate(tmp_path, "my_crate")
111
    source = b"""\
112
/// @relation(REQ-1, scope=function)
113
pub fn add(a: i32, b: i32) -> i32 {
114
    a + b
115
}
116
"""
117
    info = _read(source, file_path=str(tmp_path / "src" / "lib.rs"))
118
    function = _function_named(info, "my_crate::add")
119
    assert function.markers[0].reqs == ["REQ-1"]
120
 
121
 
122
def test_function_range_folds_in_leading_attributes_and_doc_comment():
123
    source = b"""\
124
#[cfg(test)]
125
mod tests {
126
    /// @relation(REQ-1, scope=function)
127
    #[test]
128
    fn add_works() {
129
        assert_eq!(2 + 2, 4);
130
    }
131
}
132
"""
133
    info = _read(source, file_path="lib.rs")
134
    # doc comment = 3, #[test] = 4, fn = 5, closing brace = 7.
135
    fn = _function_named(info, "lib::tests::add_works")
136
    assert fn.line_begin == 3
137
    assert fn.line_end == 7
138
    module = _function_named(info, "lib::tests")
139
    assert module.line_begin == 1
140
 
141
 
142
def test_canonical_path_crate_root_lib(tmp_path):
143
    _crate(tmp_path, "my_crate")
144
    fn = _read_one(
145
        str(tmp_path / "src" / "lib.rs"), b"pub fn add() {}\n", "my_crate::add"
146
    )
147
    assert fn.markers[0].reqs == ["REQ-1"]
148
 
149
 
150
def test_canonical_path_crate_root_main(tmp_path):
151
    _crate(tmp_path, "my_crate")
152
    _read_one(
153
        str(tmp_path / "src" / "main.rs"),
154
        b"pub fn helper() {}\n",
155
        "my_crate::helper",
156
    )
157
 
158
 
159
def test_canonical_path_top_level_module_file(tmp_path):
160
    _crate(tmp_path, "my_crate")
161
    _read_one(
162
        str(tmp_path / "src" / "model.rs"),
163
        b"pub fn parse() {}\n",
164
        "my_crate::model::parse",
165
    )
166
 
167
 
168
def test_canonical_path_nested_module_file(tmp_path):
169
    _crate(tmp_path, "my_crate")
170
    _read_one(
171
        str(tmp_path / "src" / "a" / "b" / "c.rs"),
172
        b"pub fn run() {}\n",
173
        "my_crate::a::b::c::run",
174
    )
175
 
176
 
177
def test_canonical_path_mod_rs_uses_directory_name(tmp_path):
178
    _crate(tmp_path, "my_crate")
179
    _read_one(
180
        str(tmp_path / "src" / "model" / "mod.rs"),
181
        b"pub fn run() {}\n",
182
        "my_crate::model::run",
183
    )
184
 
185
 
186
def test_canonical_path_submodule_file(tmp_path):
187
    _crate(tmp_path, "my_crate")
188
    _read_one(
189
        str(tmp_path / "src" / "model" / "tests.rs"),
190
        b"pub fn round_trips() {}\n",
191
        "my_crate::model::tests::round_trips",
192
    )
193
 
194
 
195
def test_canonical_path_inline_mod_nesting(tmp_path):
196
    _crate(tmp_path, "my_crate")
197
    source = b"""\
198
mod tests {
199
    /// @relation(REQ-3, scope=function)
200
    pub fn it_works() {}
201
}
202
"""
203
    info = _read(source, file_path=str(tmp_path / "src" / "element_id.rs"))
204
    fn = _function_named(info, "my_crate::element_id::tests::it_works")
205
    assert fn.markers[0].reqs == ["REQ-3"]
206
 
207
 
208
def test_canonical_path_integration_test_root(tmp_path):
209
    _crate(tmp_path, "my_crate")
210
    _read_one(
211
        str(tmp_path / "tests" / "it.rs"),
212
        b"fn it_runs() {}\n",
213
        "my_crate::it::it_runs",
214
    )
215
 
216
 
217
def test_canonical_path_integration_test_submodule(tmp_path):
218
    _crate(tmp_path, "my_crate")
219
    _read_one(
220
        str(tmp_path / "tests" / "it" / "helper.rs"),
221
        b"fn helps() {}\n",
222
        "my_crate::it::helper::helps",
223
    )
224
 
225
 
226
def test_canonical_path_non_cargo_flat_file_uses_stem(tmp_path):
227
    fn = _read_one(
228
        str(tmp_path / "forward_relations.rs"),
229
        b"pub fn parse() {}\n",
230
        "forward_relations::parse",
231
    )
232
    assert fn.markers[0].reqs == ["REQ-1"]
233
 
234
 
235
def test_canonical_path_non_cargo_nested_modules(tmp_path):
236
    source = b"""\
237
mod foo_module {
238
    mod foo {
239
        /// @relation(REQ-2, scope=function)
240
        pub fn bar() {}
241
    }
242
}
243
"""
244
    info = _read(source, file_path=str(tmp_path / "forward_relations.rs"))
245
    fn = _function_named(info, "forward_relations::foo_module::foo::bar")
246
    assert fn.markers[0].reqs == ["REQ-2"]
247
 
248
 
249
def test_canonical_path_workspace_members_stay_distinct(tmp_path):
250
    (tmp_path / "Cargo.toml").write_text(
251
        '[workspace]\nmembers = ["crates/foo", "crates/bar"]\n',
252
        encoding="utf-8",
253
    )
254
    _crate(tmp_path / "crates" / "foo", "foo-crate")
255
    _crate(tmp_path / "crates" / "bar", "bar-crate")
256
    body = b"""\
257
mod tests {
258
    /// @relation(REQ-1, scope=function)
259
    pub fn common_test() {}
260
}
261
"""
262
    foo = _read(
263
        body, file_path=str(tmp_path / "crates" / "foo" / "src" / "lib.rs")
264
    )
265
    bar = _read(
266
        body, file_path=str(tmp_path / "crates" / "bar" / "src" / "lib.rs")
267
    )
268
    _function_named(foo, "foo-crate::tests::common_test")
269
    _function_named(bar, "bar-crate::tests::common_test")