swh_graph/views/spy.rs
1// Copyright (C) 2024-2026 The Software Heritage developers
2// See the AUTHORS file at the top-level directory of this distribution
3// License: GNU General Public License version 3, or any later version
4// See top-level LICENSE file for more information
5
6use std::collections::HashMap;
7use std::path::Path;
8use std::sync::{Arc, Mutex};
9
10use anyhow::Result;
11
12use crate::graph::*;
13use crate::properties;
14use crate::NodeType;
15
16/// Wraps a graph, and records calls to its methods, useful for tests
17#[derive(Clone, Debug)]
18pub struct GraphSpy<G: SwhGraph> {
19 pub graph: G,
20 /// Pairs of `(method, arguments)`
21 pub history: Arc<Mutex<Vec<(&'static str, String)>>>,
22}
23
24impl<G: SwhGraph> GraphSpy<G> {
25 pub fn new(graph: G) -> Self {
26 GraphSpy {
27 graph,
28 history: Arc::new(Mutex::new(Vec::new())),
29 }
30 }
31
32 #[inline(always)]
33 fn record<Args: std::fmt::Debug>(&self, method: &'static str, arguments: Args) {
34 self.history
35 .lock()
36 .unwrap()
37 .push((method, format!("{arguments:?}")));
38 }
39}
40
41impl<G: SwhGraph> SwhGraph for GraphSpy<G> {
42 #[inline(always)]
43 fn path(&self) -> &Path {
44 self.record("path", ());
45 self.graph.path()
46 }
47 #[inline(always)]
48 fn is_transposed(&self) -> bool {
49 self.record("is_transposed", ());
50 self.graph.is_transposed()
51 }
52 #[inline(always)]
53 fn num_nodes(&self) -> usize {
54 self.record("num_nodes", ());
55 self.graph.num_nodes()
56 }
57 #[inline(always)]
58 fn has_node(&self, node_id: NodeId) -> bool {
59 self.record("has_node", (node_id,));
60 self.graph.has_node(node_id)
61 }
62 #[inline(always)]
63 fn num_arcs(&self) -> u64 {
64 self.record("num_arcs", ());
65 self.graph.num_arcs()
66 }
67 fn num_nodes_by_type(&self) -> Result<HashMap<NodeType, usize>> {
68 self.record("num_nodes_by_type", ());
69 self.graph.num_nodes_by_type()
70 }
71 fn num_arcs_by_type(&self) -> Result<HashMap<(NodeType, NodeType), usize>> {
72 self.record("num_arcs_by_type", ());
73 self.graph.num_arcs_by_type()
74 }
75 #[inline(always)]
76 fn has_arc(&self, src_node_id: NodeId, dst_node_id: NodeId) -> bool {
77 self.record("has_arc", (src_node_id, dst_node_id));
78 self.graph.has_arc(src_node_id, dst_node_id)
79 }
80}
81
82impl<G: SwhForwardGraph> SwhForwardGraph for GraphSpy<G> {
83 type Successors<'succ>
84 = <G as SwhForwardGraph>::Successors<'succ>
85 where
86 Self: 'succ;
87
88 #[inline(always)]
89 fn successors(&self, node_id: NodeId) -> Self::Successors<'_> {
90 self.record("successors", (node_id,));
91 self.graph.successors(node_id)
92 }
93 #[inline(always)]
94 fn outdegree(&self, node_id: NodeId) -> usize {
95 self.record("outdegree", (node_id,));
96 self.graph.outdegree(node_id)
97 }
98}
99
100impl<G: SwhLabeledForwardGraph> SwhLabeledForwardGraph for GraphSpy<G> {
101 type LabeledArcs<'arc>
102 = <G as SwhLabeledForwardGraph>::LabeledArcs<'arc>
103 where
104 Self: 'arc;
105 type LabeledSuccessors<'succ>
106 = <G as SwhLabeledForwardGraph>::LabeledSuccessors<'succ>
107 where
108 Self: 'succ;
109
110 fn untyped_labeled_successors(&self, node_id: NodeId) -> Self::LabeledSuccessors<'_> {
111 self.record("untyped_labeled_successors", (node_id,));
112 self.graph.untyped_labeled_successors(node_id)
113 }
114}
115
116impl<G: SwhBackwardGraph> SwhBackwardGraph for GraphSpy<G> {
117 type Predecessors<'succ>
118 = <G as SwhBackwardGraph>::Predecessors<'succ>
119 where
120 Self: 'succ;
121
122 fn predecessors(&self, node_id: NodeId) -> Self::Predecessors<'_> {
123 self.record("predecessors", (node_id,));
124 self.graph.predecessors(node_id)
125 }
126 fn indegree(&self, node_id: NodeId) -> usize {
127 self.record("indegree", (node_id,));
128 self.graph.indegree(node_id)
129 }
130}
131
132impl<G: SwhLabeledBackwardGraph> SwhLabeledBackwardGraph for GraphSpy<G> {
133 type LabeledArcs<'arc>
134 = <G as SwhLabeledBackwardGraph>::LabeledArcs<'arc>
135 where
136 Self: 'arc;
137 type LabeledPredecessors<'succ>
138 = <G as SwhLabeledBackwardGraph>::LabeledPredecessors<'succ>
139 where
140 Self: 'succ;
141
142 fn untyped_labeled_predecessors(&self, node_id: NodeId) -> Self::LabeledPredecessors<'_> {
143 self.record("untyped_labeled_predecessors", (node_id,));
144 self.graph.untyped_labeled_predecessors(node_id)
145 }
146}
147
148impl<G: SwhGraphWithProperties> SwhGraphWithProperties for GraphSpy<G> {
149 type Maps = <G as SwhGraphWithProperties>::Maps;
150 type Timestamps = <G as SwhGraphWithProperties>::Timestamps;
151 type Persons = <G as SwhGraphWithProperties>::Persons;
152 type Contents = <G as SwhGraphWithProperties>::Contents;
153 type Strings = <G as SwhGraphWithProperties>::Strings;
154 type LabelNames = <G as SwhGraphWithProperties>::LabelNames;
155
156 fn properties(
157 &self,
158 ) -> &properties::SwhGraphProperties<
159 Self::Maps,
160 Self::Timestamps,
161 Self::Persons,
162 Self::Contents,
163 Self::Strings,
164 Self::LabelNames,
165 > {
166 self.record("properties", ());
167 self.graph.properties()
168 }
169}
170
171/* TODO:
172impl<G: SwhGraphWithProperties> SwhGraphWithProperties for GraphSpy<G>
173where
174 <G as SwhGraphWithProperties>::Maps: Maps,
175 <G as SwhGraphWithProperties>::Timestamps: Timestamps,
176 <G as SwhGraphWithProperties>::Persons: Persons,
177 <G as SwhGraphWithProperties>::Contents: Contents,
178 <G as SwhGraphWithProperties>::Strings: Strings,
179 <G as SwhGraphWithProperties>::LabelNames: LabelNames,
180{
181 type Maps = PropertiesSpy<<G as SwhGraphWithProperties>::Maps>;
182 type Timestamps = PropertiesSpy<<G as SwhGraphWithProperties>::Timestamps>;
183 type Persons = PropertiesSpy<<G as SwhGraphWithProperties>::Persons>;
184 type Contents = PropertiesSpy<<G as SwhGraphWithProperties>::Contents>;
185 type Strings = PropertiesSpy<<G as SwhGraphWithProperties>::Strings>;
186 type LabelNames = PropertiesSpy<<G as SwhGraphWithProperties>::LabelNames>;
187
188 fn properties(&self) -> ??? {
189 self.record("properties", ());
190 let SwhGraphProperties {
191 path,
192 num_nodes,
193 maps,
194 timestamps,
195 persons,
196 contents,
197 strings,
198 label_names,
199 } = *self.graph.properties();
200 Arc::new(SwhGraphProperties {
201 path,
202 num_nodes,
203 maps: PropertiesSpy {
204 inner: maps,
205 history: self.history.clone(),
206 },
207 timestamps: PropertiesSpy {
208 inner: timestamps,
209 history: self.history.clone(),
210 },
211 persons: PropertiesSpy {
212 inner: persons,
213 history: self.history.clone(),
214 },
215 contents: PropertiesSpy {
216 inner: contents,
217 history: self.history.clone(),
218 },
219 strings: PropertiesSpy {
220 inner: strings,
221 history: self.history.clone(),
222 },
223 label_names: PropertiesSpy {
224 inner: label_names,
225 history: self.history.clone(),
226 },
227 })
228 }
229}
230
231pub struct PropertiesSpy<P> {
232 pub inner: P,
233 /// Pairs of `(method, arguments)`
234 pub history: Arc<Mutex<Vec<(&'static str, String)>>>,
235}
236
237impl<P> PropertiesSpy<P> {
238 fn record<Args: std::fmt::Debug>(&self, method: &'static str, arguments: Args) {
239 self.history
240 .lock()
241 .unwrap()
242 .push((method, format!("{:?}", arguments)));
243 }
244}
245
246impl<C: Contents> Contents for PropertiesSpy<C> {
247 type Data<'a> = <C as Contents>::Data<'a> where C: 'a;
248
249 fn is_skipped_content(&self) -> Self::Data<'_> {
250 self.record("is_skipped_content", ());
251 self.inner.is_skipped_content()
252 }
253 fn content_length(&self) -> Self::Data<'_> {
254 self.record("content_length", ());
255 self.inner.content_length()
256 }
257}
258
259impl<L: LabelNames> LabelNames for PropertiesSpy<L> {
260 type LabelNames<'a> = <L as LabelNames>::LabelNames<'a> where L: 'a;
261
262 fn label_names(&self) -> Self::LabelNames<'_> {
263 self.record("label_names", ());
264 self.inner.label_names()
265 }
266}
267
268impl<M: Maps> Maps for PropertiesSpy<M> {
269 type MPHF = <M as Maps>::MPHF;
270 type Perm = <M as Maps>::Perm;
271 type Memory = <M as Maps>::Memory;
272
273 fn mphf(&self) -> &Self::MPHF {
274 self.record("mphf", ());
275 self.inner.mphf()
276 }
277 fn order(&self) -> &Self::Perm {
278 self.record("order", ());
279 self.inner.order()
280 }
281 fn node2swhid(&self) -> &Node2SWHID<Self::Memory> {
282 self.record("node2swhid", ());
283 self.inner.node2swhid()
284 }
285 fn node2type(&self) -> &Node2Type<UsizeMmap<Self::Memory>> {
286 self.record("node2type", ());
287 self.inner.node2type()
288 }
289}
290
291impl<P: Persons> Persons for PropertiesSpy<P> {
292 type PersonIds<'a> = <P as Persons>::PersonIds<'a> where P: 'a;
293
294 fn author_id(&self) -> Self::PersonIds<'_> {
295 self.record("author_id", ());
296 self.inner.author_id()
297 }
298 fn committer_id(&self) -> Self::PersonIds<'_> {
299 self.record("committer_id", ());
300 self.inner.committer_id()
301 }
302}
303
304impl<S: Strings> Strings for PropertiesSpy<S> {
305 type Offsets<'a> = <S as Strings>::Offsets<'a> where S: 'a;
306
307 fn message(&self) -> &[u8] {
308 self.record("message", ());
309 self.inner.message()
310 }
311 fn message_offset(&self) -> Self::Offsets<'_> {
312 self.record("message_offset", ());
313 self.inner.message_offset()
314 }
315 fn tag_name(&self) -> &[u8] {
316 self.record("tag_name", ());
317 self.inner.tag_name()
318 }
319 fn tag_name_offset(&self) -> Self::Offsets<'_> {
320 self.record("tag_name_offset", ());
321 self.inner.tag_name_offset()
322 }
323}
324
325impl<T: Timestamps> Timestamps for PropertiesSpy<T> {
326 type Timestamps<'a> = <T as Timestamps>::Timestamps<'a> where T: 'a;
327 type Offsets<'a> = <T as Timestamps>::Offsets<'a> where T: 'a;
328
329 fn author_timestamp(&self) -> Self::Timestamps<'_> {
330 self.record("author_timestamp", ());
331 self.inner.author_timestamp()
332 }
333 fn author_timestamp_offset(&self) -> Self::Offsets<'_> {
334 self.record("author_timestamp_offset", ());
335 self.inner.author_timestamp_offset()
336 }
337 fn committer_timestamp(&self) -> Self::Timestamps<'_> {
338 self.record("committer_timestamp", ());
339 self.inner.committer_timestamp()
340 }
341 fn committer_timestamp_offset(&self) -> Self::Offsets<'_> {
342 self.record("committer_timestamp_offset", ());
343 self.inner.committer_timestamp_offset()
344 }
345}
346*/