vcsgraph 0.3.3__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vcsgraph/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ # Copyright (C) 2005-2010 Canonical Ltd
2
+ # Copyright (C) 2018-2025 Breezy Developers
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+
18
+ """Graph algorithms for version control systems.
19
+
20
+ This package contains graph-related algorithms that are useful for
21
+ version control systems, including:
22
+
23
+ - Topological sorting (tsort)
24
+ - Graph operations (graph)
25
+ """
26
+
27
+ __all__ = [
28
+ "BaseVersionedFile",
29
+ "DictParentsProvider",
30
+ "FrozenHeadsCache",
31
+ "Graph",
32
+ "KnownGraph",
33
+ "MultiMemoryVersionedFile",
34
+ "MultiVersionedFile",
35
+ "invert_parent_map",
36
+ "topo_sort",
37
+ ]
38
+
39
+ __version__ = (0, 3, 3)
40
+
41
+ # Re-export commonly used functions and classes
42
+ from .graph import (
43
+ DictParentsProvider,
44
+ FrozenHeadsCache,
45
+ Graph,
46
+ invert_parent_map,
47
+ )
48
+ from .known_graph import KnownGraph
49
+ from .tsort import topo_sort
Binary file
vcsgraph/errors.py ADDED
@@ -0,0 +1,180 @@
1
+ # Copyright (C) 2005-2010 Canonical Ltd
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ """Exceptions for vcsgraph operations."""
18
+
19
+
20
+ class Error(Exception):
21
+ """Base class for vcsgraph exceptions."""
22
+
23
+ pass
24
+
25
+
26
+ class UnsupportedOperation(Error):
27
+ """Requested operation is not supported."""
28
+
29
+ pass
30
+
31
+
32
+ class GhostRevisionsHaveNoRevno(Error):
33
+ """When searching for revnos, we encounter a ghost."""
34
+
35
+ def __init__(self, revision_id, ghost_revision_id):
36
+ """Initialize GhostRevisionsHaveNoRevno exception.
37
+
38
+ Args:
39
+ revision_id: The revision ID being searched for.
40
+ ghost_revision_id: The ghost revision ID encountered.
41
+ """
42
+ self.revision_id = revision_id
43
+ self.ghost_revision_id = ghost_revision_id
44
+ super().__init__(
45
+ f"Ghost revision {ghost_revision_id!r} has no revno, "
46
+ f"cannot determine revno for {revision_id!r}"
47
+ )
48
+
49
+ def __eq__(self, other):
50
+ """Check equality with another GhostRevisionsHaveNoRevno instance.
51
+
52
+ Args:
53
+ other: Object to compare with.
54
+
55
+ Returns:
56
+ True if both instances have the same revision_id and ghost_revision_id.
57
+ """
58
+ if not isinstance(other, GhostRevisionsHaveNoRevno):
59
+ return False
60
+ return (
61
+ self.revision_id == other.revision_id
62
+ and self.ghost_revision_id == other.ghost_revision_id
63
+ )
64
+
65
+
66
+ class InvalidRevisionId(Error):
67
+ """Invalid revision ID."""
68
+
69
+ def __init__(self, revision_id, client):
70
+ """Initialize InvalidRevisionId exception.
71
+
72
+ Args:
73
+ revision_id: The invalid revision ID.
74
+ client: The client that encountered the invalid revision.
75
+ """
76
+ self.revision_id = revision_id
77
+ self.client = client
78
+ super().__init__(f"Invalid revision ID {revision_id!r}")
79
+
80
+ def __eq__(self, other):
81
+ """Check equality with another InvalidRevisionId instance.
82
+
83
+ Args:
84
+ other: Object to compare with.
85
+
86
+ Returns:
87
+ True if both instances have the same revision_id and client.
88
+ """
89
+ if not isinstance(other, InvalidRevisionId):
90
+ return False
91
+ return self.revision_id == other.revision_id and self.client == other.client
92
+
93
+
94
+ class NoCommonAncestor(Error):
95
+ """No common ancestor found between revisions."""
96
+
97
+ def __init__(self, revision_a, revision_b):
98
+ """Initialize NoCommonAncestor exception.
99
+
100
+ Args:
101
+ revision_a: First revision ID.
102
+ revision_b: Second revision ID.
103
+ """
104
+ self.revision_a = revision_a
105
+ self.revision_b = revision_b
106
+ super().__init__(
107
+ f"No common ancestor found between {revision_a!r} and {revision_b!r}"
108
+ )
109
+
110
+ def __eq__(self, other):
111
+ """Check equality with another NoCommonAncestor instance.
112
+
113
+ Args:
114
+ other: Object to compare with.
115
+
116
+ Returns:
117
+ True if both instances have the same revision_a and revision_b.
118
+ """
119
+ if not isinstance(other, NoCommonAncestor):
120
+ return False
121
+ return (
122
+ self.revision_a == other.revision_a and self.revision_b == other.revision_b
123
+ )
124
+
125
+
126
+ class RevisionNotPresent(Error):
127
+ """Revision not present in the graph."""
128
+
129
+ def __init__(self, revision_id, graph):
130
+ """Initialize RevisionNotPresent exception.
131
+
132
+ Args:
133
+ revision_id: The revision ID not present in the graph.
134
+ graph: The graph object where the revision was not found.
135
+ """
136
+ self.revision_id = revision_id
137
+ self.graph = graph
138
+ super().__init__(f"Revision {revision_id!r} not present in graph")
139
+
140
+ def __eq__(self, other):
141
+ """Check equality with another RevisionNotPresent instance.
142
+
143
+ Args:
144
+ other: Object to compare with.
145
+
146
+ Returns:
147
+ True if both instances have the same revision_id and graph.
148
+ """
149
+ if not isinstance(other, RevisionNotPresent):
150
+ return False
151
+ return self.revision_id == other.revision_id and self.graph == other.graph
152
+
153
+
154
+ class GraphCycleError(Error):
155
+ """Cycle detected in graph.
156
+
157
+ Raised when a cycle is detected in a directed graph that should be acyclic.
158
+ """
159
+
160
+ def __init__(self, graph):
161
+ """Initialize with the graph containing the cycle.
162
+
163
+ Args:
164
+ graph: The graph object that contains a cycle.
165
+ """
166
+ self.graph = graph
167
+ super().__init__(f"Cycle in graph {graph!r}")
168
+
169
+ def __eq__(self, other):
170
+ """Check equality with another GraphCycleError instance.
171
+
172
+ Args:
173
+ other: Object to compare with.
174
+
175
+ Returns:
176
+ True if both instances have the same graph.
177
+ """
178
+ if not isinstance(other, GraphCycleError):
179
+ return False
180
+ return self.graph == other.graph
vcsgraph/graph.py ADDED
@@ -0,0 +1,338 @@
1
+ # Copyright (C) 2007-2011 Canonical Ltd
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ """Graph algorithms for version control systems."""
18
+
19
+ __all__ = [
20
+ "CachingParentsProvider",
21
+ "CallableToParentsProviderAdapter",
22
+ "DictParentsProvider",
23
+ "FrozenHeadsCache",
24
+ "GraphThunkIdsToKeys",
25
+ "HeadsCache",
26
+ "StackedParentsProvider",
27
+ "collapse_linear_regions",
28
+ "invert_parent_map",
29
+ ]
30
+
31
+ from ._graph_rs import (
32
+ CachingParentsProvider,
33
+ CallableToParentsProviderAdapter,
34
+ DictParentsProvider,
35
+ FrozenHeadsCache,
36
+ GraphThunkIdsToKeys,
37
+ HeadsCache,
38
+ StackedParentsProvider,
39
+ _RustGraph,
40
+ collapse_linear_regions,
41
+ invert_parent_map,
42
+ )
43
+ from ._graph_rs import (
44
+ _BreadthFirstSearcher as _RustBreadthFirstSearcher,
45
+ )
46
+
47
+ # NULL_REVISION constant
48
+ NULL_REVISION = b"null:"
49
+
50
+ # DIAGRAM of terminology
51
+ # A
52
+ # /\
53
+ # B C
54
+ # | |\
55
+ # D E F
56
+ # |\/| |
57
+ # |/\|/
58
+ # G H
59
+ #
60
+ # In this diagram, relative to G and H:
61
+ # A, B, C, D, E are common ancestors.
62
+ # C, D and E are border ancestors, because each has a non-common descendant.
63
+ # D and E are least common ancestors because none of their descendants are
64
+ # common ancestors.
65
+ # C is not a least common ancestor because its descendant, E, is a common
66
+ # ancestor.
67
+ #
68
+ # The find_unique_lca algorithm will pick A in two steps:
69
+ # 1. find_lca('G', 'H') => ['D', 'E']
70
+ # 2. Since len(['D', 'E']) > 1, find_lca('D', 'E') => ['A']
71
+
72
+
73
+ class Graph:
74
+ """Provide incremental access to revision graphs.
75
+
76
+ This is the generic implementation; it is intended to be subclassed to
77
+ specialize it for other repository types.
78
+ """
79
+
80
+ def __init__(self, parents_provider):
81
+ """Construct a Graph that uses several graphs as its input.
82
+
83
+ This should not normally be invoked directly, because there may be
84
+ specialized implementations for particular repository types. See
85
+ Repository.get_graph().
86
+
87
+ :param parents_provider: An object providing a get_parent_map call
88
+ conforming to the behavior of
89
+ StackedParentsProvider.get_parent_map.
90
+ """
91
+ if getattr(parents_provider, "get_parents", None) is not None:
92
+ self.get_parents = parents_provider.get_parents
93
+ if getattr(parents_provider, "get_parent_map", None) is not None:
94
+ self.get_parent_map = parents_provider.get_parent_map
95
+ self._parents_provider = parents_provider
96
+ # Rust-backed helper for methods that have been ported. Uses the same
97
+ # parents provider; the Rust side calls back into Python via a GIL
98
+ # adapter when it needs a parent lookup.
99
+ self._rs = _RustGraph(parents_provider)
100
+
101
+ def __repr__(self):
102
+ return f"Graph({self._parents_provider!r})"
103
+
104
+ def find_lca(self, *revisions):
105
+ """Determine the lowest common ancestors of the provided revisions.
106
+
107
+ A lowest common ancestor is a common ancestor none of whose
108
+ descendants are common ancestors. In graphs, unlike trees, there may
109
+ be multiple lowest common ancestors.
110
+
111
+ This algorithm has two phases. Phase 1 identifies border ancestors,
112
+ and phase 2 filters border ancestors to determine lowest common
113
+ ancestors.
114
+
115
+ In phase 1, border ancestors are identified, using a breadth-first
116
+ search starting at the bottom of the graph. Searches are stopped
117
+ whenever a node or one of its descendants is determined to be common
118
+
119
+ In phase 2, the border ancestors are filtered to find the least
120
+ common ancestors. This is done by searching the ancestries of each
121
+ border ancestor.
122
+
123
+ Phase 2 is perfomed on the principle that a border ancestor that is
124
+ not an ancestor of any other border ancestor is a least common
125
+ ancestor.
126
+
127
+ Searches are stopped when they find a node that is determined to be a
128
+ common ancestor of all border ancestors, because this shows that it
129
+ cannot be a descendant of any border ancestor.
130
+
131
+ The scaling of this operation should be proportional to:
132
+
133
+ 1. The number of uncommon ancestors
134
+ 2. The number of border ancestors
135
+ 3. The length of the shortest path between a border ancestor and an
136
+ ancestor of all border ancestors.
137
+ """
138
+ return self._rs.find_lca(revisions)
139
+
140
+ def find_difference(self, left_revision, right_revision):
141
+ """Determine the graph difference between two revisions."""
142
+ return self._rs.find_difference(left_revision, right_revision)
143
+
144
+ def find_descendants(self, old_key, new_key):
145
+ """Find descendants of old_key that are ancestors of new_key."""
146
+ return self._rs.find_descendants(old_key, new_key)
147
+
148
+ def _find_descendant_ancestors(self, old_key, new_key):
149
+ """Find ancestors of new_key that may be descendants of old_key."""
150
+ return self._rs._find_descendant_ancestors(old_key, new_key)
151
+
152
+ def _remove_simple_descendants(self, revisions, parent_map):
153
+ """Remove revisions which are children of other ones in the set.
154
+
155
+ This doesn't do any graph searching, it just checks the immediate
156
+ parent_map to find if there are any children which can be removed.
157
+
158
+ :param revisions: A set of revision_ids
159
+ :return: A set of revision_ids with the children removed
160
+ """
161
+ return self._rs._remove_simple_descendants(revisions, parent_map)
162
+
163
+ def get_child_map(self, keys):
164
+ """Get a mapping from parents to children of the specified keys.
165
+
166
+ This is simply the inversion of get_parent_map. Only supplied keys
167
+ will be discovered as children.
168
+ :return: a dict of key:child_list for keys.
169
+ """
170
+ parent_map = self._parents_provider.get_parent_map(keys)
171
+ parent_child = {}
172
+ for child, parents in sorted(parent_map.items()):
173
+ for parent in parents:
174
+ parent_child.setdefault(parent, []).append(child)
175
+ return parent_child
176
+
177
+ def find_distance_to_null(self, target_revision_id, known_revision_ids):
178
+ """Find the left-hand distance to the NULL_REVISION.
179
+
180
+ (This can also be considered the revno of a branch at
181
+ target_revision_id.)
182
+
183
+ :param target_revision_id: A revision_id which we would like to know
184
+ the revno for.
185
+ :param known_revision_ids: [(revision_id, revno)] A list of known
186
+ revno, revision_id tuples. We'll use this to seed the search.
187
+ """
188
+ return self._rs.find_distance_to_null(target_revision_id, known_revision_ids)
189
+
190
+ def find_lefthand_distances(self, keys):
191
+ """Find the distance to null for all the keys in keys.
192
+
193
+ :param keys: keys to lookup.
194
+ :return: A dict key->distance for all of keys.
195
+ """
196
+ return self._rs.find_lefthand_distances(keys)
197
+
198
+ def find_unique_ancestors(self, unique_revision, common_revisions):
199
+ """Find the unique ancestors for a revision versus others.
200
+
201
+ This returns the ancestry of unique_revision, excluding all revisions
202
+ in the ancestry of common_revisions. If unique_revision is in the
203
+ ancestry, then the empty set will be returned.
204
+
205
+ :param unique_revision: The revision_id whose ancestry we are
206
+ interested in.
207
+ (XXX: Would this API be better if we allowed multiple revisions on
208
+ to be searched here?)
209
+ :param common_revisions: Revision_ids of ancestries to exclude.
210
+ :return: A set of revisions in the ancestry of unique_revision
211
+ """
212
+ return self._rs.find_unique_ancestors(unique_revision, common_revisions)
213
+
214
+ def get_parent_map(self, revisions): # type: ignore
215
+ """Get a map of key:parent_list for revisions.
216
+
217
+ This implementation delegates to get_parents, for old parent_providers
218
+ that do not supply get_parent_map.
219
+ """
220
+ result = {}
221
+ for rev, parents in self.get_parents(revisions):
222
+ if parents is not None:
223
+ result[rev] = parents
224
+ return result
225
+
226
+ def _make_breadth_first_searcher(self, revisions):
227
+ return _RustBreadthFirstSearcher(revisions, self)
228
+
229
+ def heads(self, keys):
230
+ """Return the heads from amongst keys.
231
+
232
+ This is done by searching the ancestries of each key. Any key that is
233
+ reachable from another key is not returned; all the others are.
234
+
235
+ This operation scales with the relative depth between any two keys. If
236
+ any two keys are completely disconnected all ancestry of both sides
237
+ will be retrieved.
238
+
239
+ :param keys: An iterable of keys.
240
+ :return: A set of the heads. Note that as a set there is no ordering
241
+ information. Callers will need to filter their input to create
242
+ order if they need it.
243
+ """
244
+ return self._rs.heads(keys)
245
+
246
+ def find_merge_order(self, tip_revision_id, lca_revision_ids):
247
+ """Find the order that each revision was merged into tip.
248
+
249
+ This basically just walks backwards with a stack, and walks left-first
250
+ until it finds a node to stop.
251
+ """
252
+ return self._rs.find_merge_order(tip_revision_id, lca_revision_ids)
253
+
254
+ def find_lefthand_merger(self, merged_key, tip_key):
255
+ """Find the first lefthand ancestor of tip_key that merged merged_key.
256
+
257
+ We do this by first finding the descendants of merged_key, then
258
+ walking through the lefthand ancestry of tip_key until we find a key
259
+ that doesn't descend from merged_key. Its child is the key that
260
+ merged merged_key.
261
+
262
+ :return: The first lefthand ancestor of tip_key to merge merged_key.
263
+ merged_key if it is a lefthand ancestor of tip_key.
264
+ None if no ancestor of tip_key merged merged_key.
265
+ """
266
+ return self._rs.find_lefthand_merger(merged_key, tip_key)
267
+
268
+ def find_unique_lca(self, left_revision, right_revision, count_steps=False):
269
+ """Find a unique LCA.
270
+
271
+ Find lowest common ancestors. If there is no unique common
272
+ ancestor, find the lowest common ancestors of those ancestors.
273
+
274
+ Iteration stops when a unique lowest common ancestor is found.
275
+ The graph origin is necessarily a unique lowest common ancestor.
276
+
277
+ Note that None is not an acceptable substitute for NULL_REVISION.
278
+ in the input for this method.
279
+
280
+ :param count_steps: If True, the return value will be a tuple of
281
+ (unique_lca, steps) where steps is the number of times that
282
+ find_lca was run. If False, only unique_lca is returned.
283
+ """
284
+ return self._rs.find_unique_lca(left_revision, right_revision, count_steps)
285
+
286
+ def iter_ancestry(self, revision_ids):
287
+ """Iterate the ancestry of this revision.
288
+
289
+ :param revision_ids: Nodes to start the search
290
+ :return: Yield tuples mapping a revision_id to its parents for the
291
+ ancestry of revision_id.
292
+ Ghosts will be returned with None as their parents, and nodes
293
+ with no parents will have NULL_REVISION as their only parent. (As
294
+ defined by get_parent_map.)
295
+ There will also be a node for (NULL_REVISION, ())
296
+ """
297
+ yield from self._rs.iter_ancestry(revision_ids)
298
+
299
+ def iter_lefthand_ancestry(self, start_key, stop_keys=None):
300
+ """Iterate the lefthand ancestry of start_key.
301
+
302
+ Yields revisions in lefthand order. Callers may break early; the
303
+ walk is lazy and does not query further than the last yielded key.
304
+ Raises RevisionNotPresent if the walk reaches a key that is not in
305
+ the parents provider.
306
+ """
307
+ return self._rs.iter_lefthand_ancestry(start_key, stop_keys)
308
+
309
+ def iter_topo_order(self, revisions):
310
+ """Iterate through the input revisions in topological order.
311
+
312
+ This sorting only ensures that parents come before their children.
313
+ An ancestor may sort after a descendant if the relationship is not
314
+ visible in the supplied list of revisions.
315
+ """
316
+ return iter(self._rs.iter_topo_order(revisions))
317
+
318
+ def is_ancestor(self, candidate_ancestor, candidate_descendant):
319
+ """Determine whether a revision is an ancestor of another.
320
+
321
+ We answer this using heads() as heads() has the logic to perform the
322
+ smallest number of parent lookups to determine the ancestral
323
+ relationship between N revisions.
324
+ """
325
+ return self._rs.is_ancestor(candidate_ancestor, candidate_descendant)
326
+
327
+ def is_between(self, revid, lower_bound_revid, upper_bound_revid):
328
+ """Determine whether a revision is between two others.
329
+
330
+ returns true if and only if:
331
+ lower_bound_revid <= revid <= upper_bound_revid
332
+ """
333
+ return self._rs.is_between(revid, lower_bound_revid, upper_bound_revid)
334
+
335
+
336
+ _counters = [0, 0, 0, 0, 0, 0, 0]
337
+
338
+ # Import KnownGraph to make it available through this module for compatibility
@@ -0,0 +1,24 @@
1
+ # Copyright (C) 2009, 2010 Canonical Ltd
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ """KnownGraph: graph algorithms for the case where the full ancestry is known.
18
+
19
+ The implementation is provided by the Rust extension module ``_graph_rs``.
20
+ """
21
+
22
+ from ._graph_rs import KnownGraph, _KnownGraphNode, _MergeSortNode
23
+
24
+ __all__ = ["KnownGraph", "_KnownGraphNode", "_MergeSortNode"]
vcsgraph/py.typed ADDED
File without changes
@@ -0,0 +1,60 @@
1
+ # Copyright (C) 2025 Breezy Developers
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ import os
18
+ import shutil
19
+ import tempfile
20
+ import unittest
21
+ from unittest import TestCase
22
+
23
+ __all__ = ["TestCaseInTempDir"]
24
+
25
+
26
+ class TestCaseInTempDir(TestCase):
27
+ """Minimal TestCase that runs in a temporary directory.
28
+
29
+ Only implements the functionality actually needed by vcsgraph tests.
30
+ """
31
+
32
+ def setUp(self):
33
+ super().setUp()
34
+ self.test_dir = tempfile.mkdtemp(prefix="vcsgraph_test_")
35
+ self.original_dir = os.getcwd()
36
+ os.chdir(self.test_dir)
37
+
38
+ def tearDown(self):
39
+ os.chdir(self.original_dir)
40
+ shutil.rmtree(self.test_dir)
41
+ super().tearDown()
42
+
43
+ def assertPathExists(self, path):
44
+ """Fail unless path exists."""
45
+ self.assertTrue(os.path.lexists(path), f"{path} does not exist")
46
+
47
+ def assertPathDoesNotExist(self, path):
48
+ """Fail if path exists."""
49
+ self.assertFalse(os.path.lexists(path), f"{path} exists")
50
+
51
+
52
+ def test_suite() -> unittest.TestSuite:
53
+ names = [
54
+ "known_graph",
55
+ "graph",
56
+ "tsort",
57
+ ]
58
+ module_names = ["vcsgraph.tests.test_" + name for name in names]
59
+ loader = unittest.TestLoader()
60
+ return loader.loadTestsFromNames(module_names)