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_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}