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