Skip to main content

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}