whitaker_common/test_support/decomposition.rs
1//! Shared decomposition-advice fixtures for unit and integration tests.
2//!
3//! These helpers keep the recurring grammar/serde/filesystem and transport
4//! method communities aligned across diagnostic unit tests and behaviour
5//! coverage.
6
7#[path = "decomposition_adjacency.rs"]
8mod adjacency;
9#[path = "decomposition_vector_algebra.rs"]
10mod vector_algebra;
11
12use crate::decomposition_advice::{
13 DecompositionContext, DecompositionSuggestion, MethodProfile, MethodProfileBuilder,
14 SubjectKind, methods_meet_cosine_threshold as runtime_methods_meet_cosine_threshold,
15 suggest_decomposition,
16};
17
18pub use self::adjacency::{AdjacencyError, AdjacencyReport, EdgeInput, adjacency_report};
19pub use self::vector_algebra::{MethodVectorAlgebraReport, method_vector_algebra};
20
21/// Input data for building a [`MethodProfile`] in tests.
22///
23/// # Examples
24///
25/// ```ignore
26/// use whitaker_common::test_support::decomposition::{MethodInput, profile};
27///
28/// let profile = profile(MethodInput {
29/// name: "parse_tokens",
30/// fields: &["grammar"],
31/// signature_types: &[],
32/// local_types: &[],
33/// domains: &[],
34/// });
35///
36/// assert_eq!(profile.name(), "parse_tokens");
37/// ```
38pub struct MethodInput<'a> {
39 pub name: &'a str,
40 pub fields: &'a [&'a str],
41 pub signature_types: &'a [&'a str],
42 pub local_types: &'a [&'a str],
43 pub domains: &'a [&'a str],
44}
45
46/// Builds a [`MethodProfile`] from declarative test input.
47///
48/// # Examples
49///
50/// ```ignore
51/// use whitaker_common::test_support::decomposition::{MethodInput, profile};
52///
53/// let profile = profile(MethodInput {
54/// name: "save_to_disk",
55/// fields: &[],
56/// signature_types: &[],
57/// local_types: &["PathBuf"],
58/// domains: &["std::fs"],
59/// });
60///
61/// assert_eq!(profile.name(), "save_to_disk");
62/// ```
63#[must_use]
64pub fn profile(input: MethodInput<'_>) -> MethodProfile {
65 let mut builder = MethodProfileBuilder::new(input.name);
66 for field in input.fields {
67 builder.record_accessed_field(*field);
68 }
69 for type_name in input.signature_types {
70 builder.record_signature_type(*type_name);
71 }
72 for type_name in input.local_types {
73 builder.record_local_type(*type_name);
74 }
75 for domain in input.domains {
76 builder.record_external_domain(*domain);
77 }
78 builder.build()
79}
80
81/// Builds the recurring parser/serde/filesystem fixture.
82///
83/// # Examples
84///
85/// ```ignore
86/// use whitaker_common::test_support::decomposition::parser_serde_fs_fixture;
87///
88/// let methods = parser_serde_fs_fixture();
89/// assert_eq!(methods.len(), 6);
90/// ```
91#[must_use]
92pub fn parser_serde_fs_fixture() -> Vec<MethodProfile> {
93 vec![
94 profile(MethodInput {
95 name: "parse_tokens",
96 fields: &["grammar", "tokens"],
97 signature_types: &["TokenStream"],
98 local_types: &[],
99 domains: &[],
100 }),
101 profile(MethodInput {
102 name: "parse_nodes",
103 fields: &["grammar", "ast"],
104 signature_types: &[],
105 local_types: &["ParseState"],
106 domains: &[],
107 }),
108 profile(MethodInput {
109 name: "encode_json",
110 fields: &[],
111 signature_types: &["Serializer"],
112 local_types: &[],
113 domains: &["serde::json"],
114 }),
115 profile(MethodInput {
116 name: "decode_json",
117 fields: &[],
118 signature_types: &["Deserializer"],
119 local_types: &[],
120 domains: &["serde::json"],
121 }),
122 profile(MethodInput {
123 name: "load_from_disk",
124 fields: &[],
125 signature_types: &[],
126 local_types: &["PathBuf"],
127 domains: &["std::fs"],
128 }),
129 profile(MethodInput {
130 name: "save_to_disk",
131 fields: &[],
132 signature_types: &[],
133 local_types: &["PathBuf"],
134 domains: &["std::fs"],
135 }),
136 ]
137}
138
139/// Builds the recurring transport trait fixture.
140///
141/// # Examples
142///
143/// ```ignore
144/// use whitaker_common::test_support::decomposition::transport_trait_fixture;
145///
146/// let methods = transport_trait_fixture();
147/// assert_eq!(methods.len(), 4);
148/// ```
149#[must_use]
150pub fn transport_trait_fixture() -> Vec<MethodProfile> {
151 vec![
152 profile(MethodInput {
153 name: "encode_request",
154 fields: &[],
155 signature_types: &[],
156 local_types: &[],
157 domains: &["serde::json"],
158 }),
159 profile(MethodInput {
160 name: "decode_request",
161 fields: &[],
162 signature_types: &[],
163 local_types: &[],
164 domains: &["serde::json"],
165 }),
166 profile(MethodInput {
167 name: "read_frame",
168 fields: &[],
169 signature_types: &["IoBuffer"],
170 local_types: &[],
171 domains: &["std::io"],
172 }),
173 profile(MethodInput {
174 name: "write_frame",
175 fields: &[],
176 signature_types: &["IoBuffer"],
177 local_types: &[],
178 domains: &["std::io"],
179 }),
180 ]
181}
182
183/// Computes decomposition suggestions for `methods`.
184///
185/// # Examples
186///
187/// ```ignore
188/// use whitaker_common::decomposition_advice::SubjectKind;
189/// use whitaker_common::test_support::decomposition::{
190/// decomposition_suggestions,
191/// parser_serde_fs_fixture,
192/// };
193///
194/// let methods = parser_serde_fs_fixture();
195/// let (_context, suggestions) =
196/// decomposition_suggestions("Foo", SubjectKind::Type, &methods);
197/// assert!(!suggestions.is_empty());
198/// ```
199#[must_use]
200pub fn decomposition_suggestions(
201 subject: &str,
202 kind: SubjectKind,
203 methods: &[MethodProfile],
204) -> (DecompositionContext, Vec<DecompositionSuggestion>) {
205 let context = DecompositionContext::new(subject, kind);
206 let suggestions = suggest_decomposition(&context, methods);
207 (context, suggestions)
208}
209
210/// Evaluates whether two methods satisfy Whitaker's cosine threshold.
211///
212/// This helper exists for behaviour tests that need an observable seam without
213/// widening the production decomposition API.
214///
215/// # Examples
216///
217/// ```ignore
218/// use whitaker_common::test_support::decomposition::{MethodInput, methods_meet_cosine_threshold, profile};
219///
220/// let left = profile(MethodInput {
221/// name: "parse_tokens",
222/// fields: &["grammar"],
223/// signature_types: &[],
224/// local_types: &[],
225/// domains: &[],
226/// });
227/// let right = profile(MethodInput {
228/// name: "parse_nodes",
229/// fields: &["grammar"],
230/// signature_types: &[],
231/// local_types: &[],
232/// domains: &[],
233/// });
234///
235/// assert!(methods_meet_cosine_threshold(&left, &right));
236/// ```
237#[must_use]
238pub fn methods_meet_cosine_threshold(left: &MethodProfile, right: &MethodProfile) -> bool {
239 runtime_methods_meet_cosine_threshold(left, right)
240}