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