Show More
@@ -1,92 +1,124 b'' | |||||
1 | // ancestors.rs |
|
1 | // ancestors.rs | |
2 | // |
|
2 | // | |
3 | // Copyright 2018 Georges Racinet <gracinet@anybox.fr> |
|
3 | // Copyright 2018 Georges Racinet <gracinet@anybox.fr> | |
4 | // |
|
4 | // | |
5 | // This software may be used and distributed according to the terms of the |
|
5 | // This software may be used and distributed according to the terms of the | |
6 | // GNU General Public License version 2 or any later version. |
|
6 | // GNU General Public License version 2 or any later version. | |
7 |
|
7 | |||
8 | //! Bindings for the hg::ancestors module provided by the |
|
8 | //! Bindings for the hg::ancestors module provided by the | |
9 | //! `hg-core` crate. From Python, this will be seen as `rustext.ancestor` |
|
9 | //! `hg-core` crate. From Python, this will be seen as `rustext.ancestor` | |
10 | use cindex::Index; |
|
10 | use cindex::Index; | |
11 | use cpython::{ |
|
11 | use cpython::{ | |
12 | ObjectProtocol, PyClone, PyDict, PyModule, PyObject, PyResult, Python, |
|
12 | ObjectProtocol, PyClone, PyDict, PyModule, PyObject, PyResult, Python, | |
13 | }; |
|
13 | }; | |
14 | use exceptions::GraphError; |
|
14 | use exceptions::GraphError; | |
15 | use hg; |
|
15 | use hg; | |
16 | use hg::AncestorsIterator as CoreIterator; |
|
|||
17 | use hg::Revision; |
|
16 | use hg::Revision; | |
|
17 | use hg::{AncestorsIterator as CoreIterator, LazyAncestors as CoreLazy}; | |||
18 | use std::cell::RefCell; |
|
18 | use std::cell::RefCell; | |
19 |
|
19 | |||
20 | /// Utility function to convert a Python iterable into a Vec<Revision> |
|
20 | /// Utility function to convert a Python iterable into a Vec<Revision> | |
21 | /// |
|
21 | /// | |
22 | /// We need this to feed to AncestorIterators constructors because |
|
22 | /// We need this to feed to AncestorIterators constructors because | |
23 | /// a PyErr can arise at each step of iteration, whereas our inner objects |
|
23 | /// a PyErr can arise at each step of iteration, whereas our inner objects | |
24 | /// expect iterables over Revision, not over some Result<Revision, PyErr> |
|
24 | /// expect iterables over Revision, not over some Result<Revision, PyErr> | |
25 | fn reviter_to_revvec(py: Python, revs: PyObject) -> PyResult<Vec<Revision>> { |
|
25 | fn reviter_to_revvec(py: Python, revs: PyObject) -> PyResult<Vec<Revision>> { | |
26 | revs.iter(py)? |
|
26 | revs.iter(py)? | |
27 | .map(|r| r.and_then(|o| o.extract::<Revision>(py))) |
|
27 | .map(|r| r.and_then(|o| o.extract::<Revision>(py))) | |
28 | .collect() |
|
28 | .collect() | |
29 | } |
|
29 | } | |
30 |
|
30 | |||
31 | py_class!(class AncestorsIterator |py| { |
|
31 | py_class!(class AncestorsIterator |py| { | |
32 | // TODO RW lock ? |
|
32 | // TODO RW lock ? | |
33 | data inner: RefCell<Box<CoreIterator<Index>>>; |
|
33 | data inner: RefCell<Box<CoreIterator<Index>>>; | |
34 |
|
34 | |||
35 | def __next__(&self) -> PyResult<Option<Revision>> { |
|
35 | def __next__(&self) -> PyResult<Option<Revision>> { | |
36 | match self.inner(py).borrow_mut().next() { |
|
36 | match self.inner(py).borrow_mut().next() { | |
37 | Some(Err(e)) => Err(GraphError::pynew(py, e)), |
|
37 | Some(Err(e)) => Err(GraphError::pynew(py, e)), | |
38 | None => Ok(None), |
|
38 | None => Ok(None), | |
39 | Some(Ok(r)) => Ok(Some(r)), |
|
39 | Some(Ok(r)) => Ok(Some(r)), | |
40 | } |
|
40 | } | |
41 | } |
|
41 | } | |
42 |
|
42 | |||
43 | def __contains__(&self, rev: Revision) -> PyResult<bool> { |
|
43 | def __contains__(&self, rev: Revision) -> PyResult<bool> { | |
44 | self.inner(py).borrow_mut().contains(rev).map_err(|e| GraphError::pynew(py, e)) |
|
44 | self.inner(py).borrow_mut().contains(rev).map_err(|e| GraphError::pynew(py, e)) | |
45 | } |
|
45 | } | |
46 |
|
46 | |||
47 | def __iter__(&self) -> PyResult<Self> { |
|
47 | def __iter__(&self) -> PyResult<Self> { | |
48 | Ok(self.clone_ref(py)) |
|
48 | Ok(self.clone_ref(py)) | |
49 | } |
|
49 | } | |
50 |
|
50 | |||
51 | def __new__(_cls, index: PyObject, initrevs: PyObject, stoprev: Revision, |
|
51 | def __new__(_cls, index: PyObject, initrevs: PyObject, stoprev: Revision, | |
52 | inclusive: bool) -> PyResult<AncestorsIterator> { |
|
52 | inclusive: bool) -> PyResult<AncestorsIterator> { | |
53 | let initvec = reviter_to_revvec(py, initrevs)?; |
|
53 | let initvec = reviter_to_revvec(py, initrevs)?; | |
54 | let ait = match hg::AncestorsIterator::new(Index::new(py, index)?, |
|
54 | let ait = match hg::AncestorsIterator::new(Index::new(py, index)?, | |
55 | initvec, stoprev, |
|
55 | initvec, stoprev, | |
56 | inclusive) { |
|
56 | inclusive) { | |
57 | Ok(ait) => ait, |
|
57 | Ok(ait) => ait, | |
58 | Err(e) => { |
|
58 | Err(e) => { | |
59 | return Err(GraphError::pynew(py, e)); |
|
59 | return Err(GraphError::pynew(py, e)); | |
60 | } |
|
60 | } | |
61 | }; |
|
61 | }; | |
62 | AncestorsIterator::from_inner(py, ait) |
|
62 | AncestorsIterator::from_inner(py, ait) | |
63 | } |
|
63 | } | |
64 |
|
64 | |||
65 | }); |
|
65 | }); | |
66 |
|
66 | |||
67 | impl AncestorsIterator { |
|
67 | impl AncestorsIterator { | |
68 | pub fn from_inner(py: Python, ait: CoreIterator<Index>) -> PyResult<Self> { |
|
68 | pub fn from_inner(py: Python, ait: CoreIterator<Index>) -> PyResult<Self> { | |
69 | Self::create_instance(py, RefCell::new(Box::new(ait))) |
|
69 | Self::create_instance(py, RefCell::new(Box::new(ait))) | |
70 | } |
|
70 | } | |
71 | } |
|
71 | } | |
72 |
|
72 | |||
|
73 | py_class!(class LazyAncestors |py| { | |||
|
74 | data inner: RefCell<Box<CoreLazy<Index>>>; | |||
|
75 | ||||
|
76 | def __contains__(&self, rev: Revision) -> PyResult<bool> { | |||
|
77 | self.inner(py) | |||
|
78 | .borrow_mut() | |||
|
79 | .contains(rev) | |||
|
80 | .map_err(|e| GraphError::pynew(py, e)) | |||
|
81 | } | |||
|
82 | ||||
|
83 | def __iter__(&self) -> PyResult<AncestorsIterator> { | |||
|
84 | AncestorsIterator::from_inner(py, self.inner(py).borrow().iter()) | |||
|
85 | } | |||
|
86 | ||||
|
87 | def __bool__(&self) -> PyResult<bool> { | |||
|
88 | Ok(!self.inner(py).borrow().is_empty()) | |||
|
89 | } | |||
|
90 | ||||
|
91 | def __new__(_cls, index: PyObject, initrevs: PyObject, stoprev: Revision, | |||
|
92 | inclusive: bool) -> PyResult<Self> { | |||
|
93 | let initvec = reviter_to_revvec(py, initrevs)?; | |||
|
94 | ||||
|
95 | let lazy = | |||
|
96 | CoreLazy::new(Index::new(py, index)?, initvec, stoprev, inclusive) | |||
|
97 | .map_err(|e| GraphError::pynew(py, e))?; | |||
|
98 | ||||
|
99 | Self::create_instance(py, RefCell::new(Box::new(lazy))) | |||
|
100 | } | |||
|
101 | ||||
|
102 | }); | |||
|
103 | ||||
73 | /// Create the module, with __package__ given from parent |
|
104 | /// Create the module, with __package__ given from parent | |
74 | pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> { |
|
105 | pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> { | |
75 | let dotted_name = &format!("{}.ancestor", package); |
|
106 | let dotted_name = &format!("{}.ancestor", package); | |
76 | let m = PyModule::new(py, dotted_name)?; |
|
107 | let m = PyModule::new(py, dotted_name)?; | |
77 | m.add(py, "__package__", package)?; |
|
108 | m.add(py, "__package__", package)?; | |
78 | m.add( |
|
109 | m.add( | |
79 | py, |
|
110 | py, | |
80 | "__doc__", |
|
111 | "__doc__", | |
81 | "Generic DAG ancestor algorithms - Rust implementation", |
|
112 | "Generic DAG ancestor algorithms - Rust implementation", | |
82 | )?; |
|
113 | )?; | |
83 | m.add_class::<AncestorsIterator>(py)?; |
|
114 | m.add_class::<AncestorsIterator>(py)?; | |
|
115 | m.add_class::<LazyAncestors>(py)?; | |||
84 |
|
116 | |||
85 | let sys = PyModule::import(py, "sys")?; |
|
117 | let sys = PyModule::import(py, "sys")?; | |
86 | let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?; |
|
118 | let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?; | |
87 | sys_modules.set_item(py, dotted_name, &m)?; |
|
119 | sys_modules.set_item(py, dotted_name, &m)?; | |
88 | // Example C code (see pyexpat.c and import.c) will "give away the |
|
120 | // Example C code (see pyexpat.c and import.c) will "give away the | |
89 | // reference", but we won't because it will be consumed once the |
|
121 | // reference", but we won't because it will be consumed once the | |
90 | // Rust PyObject is dropped. |
|
122 | // Rust PyObject is dropped. | |
91 | Ok(m) |
|
123 | Ok(m) | |
92 | } |
|
124 | } |
@@ -1,107 +1,141 b'' | |||||
1 | from __future__ import absolute_import |
|
1 | from __future__ import absolute_import | |
2 | import sys |
|
2 | import sys | |
3 | import unittest |
|
3 | import unittest | |
4 |
|
4 | |||
5 | try: |
|
5 | try: | |
6 | from mercurial import rustext |
|
6 | from mercurial import rustext | |
7 | rustext.__name__ # trigger immediate actual import |
|
7 | rustext.__name__ # trigger immediate actual import | |
8 | except ImportError: |
|
8 | except ImportError: | |
9 | rustext = None |
|
9 | rustext = None | |
10 | else: |
|
10 | else: | |
11 | # this would fail already without appropriate ancestor.__package__ |
|
11 | # this would fail already without appropriate ancestor.__package__ | |
12 |
from mercurial.rustext.ancestor import |
|
12 | from mercurial.rustext.ancestor import ( | |
|
13 | AncestorsIterator, | |||
|
14 | LazyAncestors | |||
|
15 | ) | |||
13 |
|
16 | |||
14 | try: |
|
17 | try: | |
15 | from mercurial.cext import parsers as cparsers |
|
18 | from mercurial.cext import parsers as cparsers | |
16 | except ImportError: |
|
19 | except ImportError: | |
17 | cparsers = None |
|
20 | cparsers = None | |
18 |
|
21 | |||
19 | # picked from test-parse-index2, copied rather than imported |
|
22 | # picked from test-parse-index2, copied rather than imported | |
20 | # so that it stays stable even if test-parse-index2 changes or disappears. |
|
23 | # so that it stays stable even if test-parse-index2 changes or disappears. | |
21 | data_non_inlined = ( |
|
24 | data_non_inlined = ( | |
22 | b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01D\x19' |
|
25 | b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01D\x19' | |
23 | b'\x00\x07e\x12\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff' |
|
26 | b'\x00\x07e\x12\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff' | |
24 | b'\xff\xff\xff\xff\xd1\xf4\xbb\xb0\xbe\xfc\x13\xbd\x8c\xd3\x9d' |
|
27 | b'\xff\xff\xff\xff\xd1\xf4\xbb\xb0\xbe\xfc\x13\xbd\x8c\xd3\x9d' | |
25 | b'\x0f\xcd\xd9;\x8c\x07\x8cJ/\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
28 | b'\x0f\xcd\xd9;\x8c\x07\x8cJ/\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
26 | b'\x00\x00\x00\x00\x00\x00\x01D\x19\x00\x00\x00\x00\x00\xdf\x00' |
|
29 | b'\x00\x00\x00\x00\x00\x00\x01D\x19\x00\x00\x00\x00\x00\xdf\x00' | |
27 | b'\x00\x01q\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xff' |
|
30 | b'\x00\x01q\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xff' | |
28 | b'\xff\xff\xff\xc1\x12\xb9\x04\x96\xa4Z1t\x91\xdfsJ\x90\xf0\x9bh' |
|
31 | b'\xff\xff\xff\xc1\x12\xb9\x04\x96\xa4Z1t\x91\xdfsJ\x90\xf0\x9bh' | |
29 | b'\x07l&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
32 | b'\x07l&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
30 | b'\x00\x01D\xf8\x00\x00\x00\x00\x01\x1b\x00\x00\x01\xb8\x00\x00' |
|
33 | b'\x00\x01D\xf8\x00\x00\x00\x00\x01\x1b\x00\x00\x01\xb8\x00\x00' | |
31 | b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\xff\xff\xff\xff\x02\n' |
|
34 | b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\xff\xff\xff\xff\x02\n' | |
32 | b'\x0e\xc6&\xa1\x92\xae6\x0b\x02i\xfe-\xe5\xbao\x05\xd1\xe7\x00' |
|
35 | b'\x0e\xc6&\xa1\x92\xae6\x0b\x02i\xfe-\xe5\xbao\x05\xd1\xe7\x00' | |
33 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01F' |
|
36 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01F' | |
34 | b'\x13\x00\x00\x00\x00\x01\xec\x00\x00\x03\x06\x00\x00\x00\x01' |
|
37 | b'\x13\x00\x00\x00\x00\x01\xec\x00\x00\x03\x06\x00\x00\x00\x01' | |
35 | b'\x00\x00\x00\x03\x00\x00\x00\x02\xff\xff\xff\xff\x12\xcb\xeby1' |
|
38 | b'\x00\x00\x00\x03\x00\x00\x00\x02\xff\xff\xff\xff\x12\xcb\xeby1' | |
36 | b'\xb6\r\x98B\xcb\x07\xbd`\x8f\x92\xd9\xc4\x84\xbdK\x00\x00\x00' |
|
39 | b'\xb6\r\x98B\xcb\x07\xbd`\x8f\x92\xd9\xc4\x84\xbdK\x00\x00\x00' | |
37 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
40 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
38 | ) |
|
41 | ) | |
39 |
|
42 | |||
40 |
|
43 | |||
41 | @unittest.skipIf(rustext is None or cparsers is None, |
|
44 | @unittest.skipIf(rustext is None or cparsers is None, | |
42 | "rustext or the C Extension parsers module " |
|
45 | "rustext or the C Extension parsers module " | |
43 | "ancestor relies on is not available") |
|
46 | "ancestor relies on is not available") | |
44 | class rustancestorstest(unittest.TestCase): |
|
47 | class rustancestorstest(unittest.TestCase): | |
45 | """Test the correctness of binding to Rust code. |
|
48 | """Test the correctness of binding to Rust code. | |
46 |
|
49 | |||
47 | This test is merely for the binding to Rust itself: extraction of |
|
50 | This test is merely for the binding to Rust itself: extraction of | |
48 | Python variable, giving back the results etc. |
|
51 | Python variable, giving back the results etc. | |
49 |
|
52 | |||
50 | It is not meant to test the algorithmic correctness of the operations |
|
53 | It is not meant to test the algorithmic correctness of the operations | |
51 | on ancestors it provides. Hence the very simple embedded index data is |
|
54 | on ancestors it provides. Hence the very simple embedded index data is | |
52 | good enough. |
|
55 | good enough. | |
53 |
|
56 | |||
54 | Algorithmic correctness is asserted by the Rust unit tests. |
|
57 | Algorithmic correctness is asserted by the Rust unit tests. | |
55 | """ |
|
58 | """ | |
56 |
|
59 | |||
57 | def parseindex(self): |
|
60 | def parseindex(self): | |
58 | return cparsers.parse_index2(data_non_inlined, False)[0] |
|
61 | return cparsers.parse_index2(data_non_inlined, False)[0] | |
59 |
|
62 | |||
60 | def testiteratorrevlist(self): |
|
63 | def testiteratorrevlist(self): | |
61 | idx = self.parseindex() |
|
64 | idx = self.parseindex() | |
62 | # checking test assumption about the index binary data: |
|
65 | # checking test assumption about the index binary data: | |
63 | self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)}, |
|
66 | self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)}, | |
64 | {0: (-1, -1), |
|
67 | {0: (-1, -1), | |
65 | 1: (0, -1), |
|
68 | 1: (0, -1), | |
66 | 2: (1, -1), |
|
69 | 2: (1, -1), | |
67 | 3: (2, -1)}) |
|
70 | 3: (2, -1)}) | |
68 | ait = AncestorsIterator(idx, [3], 0, True) |
|
71 | ait = AncestorsIterator(idx, [3], 0, True) | |
69 | self.assertEqual([r for r in ait], [3, 2, 1, 0]) |
|
72 | self.assertEqual([r for r in ait], [3, 2, 1, 0]) | |
70 |
|
73 | |||
71 | ait = AncestorsIterator(idx, [3], 0, False) |
|
74 | ait = AncestorsIterator(idx, [3], 0, False) | |
72 | self.assertEqual([r for r in ait], [2, 1, 0]) |
|
75 | self.assertEqual([r for r in ait], [2, 1, 0]) | |
73 |
|
76 | |||
|
77 | def testlazyancestors(self): | |||
|
78 | idx = self.parseindex() | |||
|
79 | start_count = sys.getrefcount(idx) # should be 2 (see Python doc) | |||
|
80 | self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)}, | |||
|
81 | {0: (-1, -1), | |||
|
82 | 1: (0, -1), | |||
|
83 | 2: (1, -1), | |||
|
84 | 3: (2, -1)}) | |||
|
85 | lazy = LazyAncestors(idx, [3], 0, True) | |||
|
86 | # we have two more references to the index: | |||
|
87 | # - in its inner iterator for __contains__ and __bool__ | |||
|
88 | # - in the LazyAncestors instance itself (to spawn new iterators) | |||
|
89 | self.assertEqual(sys.getrefcount(idx), start_count + 2) | |||
|
90 | ||||
|
91 | self.assertTrue(2 in lazy) | |||
|
92 | self.assertTrue(bool(lazy)) | |||
|
93 | self.assertEqual(list(lazy), [3, 2, 1, 0]) | |||
|
94 | # a second time to validate that we spawn new iterators | |||
|
95 | self.assertEqual(list(lazy), [3, 2, 1, 0]) | |||
|
96 | ||||
|
97 | # now let's watch the refcounts closer | |||
|
98 | ait = iter(lazy) | |||
|
99 | self.assertEqual(sys.getrefcount(idx), start_count + 3) | |||
|
100 | del ait | |||
|
101 | self.assertEqual(sys.getrefcount(idx), start_count + 2) | |||
|
102 | del lazy | |||
|
103 | self.assertEqual(sys.getrefcount(idx), start_count) | |||
|
104 | ||||
|
105 | # let's check bool for an empty one | |||
|
106 | self.assertFalse(LazyAncestors(idx, [0], 0, False)) | |||
|
107 | ||||
74 | def testrefcount(self): |
|
108 | def testrefcount(self): | |
75 | idx = self.parseindex() |
|
109 | idx = self.parseindex() | |
76 | start_count = sys.getrefcount(idx) |
|
110 | start_count = sys.getrefcount(idx) | |
77 |
|
111 | |||
78 | # refcount increases upon iterator init... |
|
112 | # refcount increases upon iterator init... | |
79 | ait = AncestorsIterator(idx, [3], 0, True) |
|
113 | ait = AncestorsIterator(idx, [3], 0, True) | |
80 | self.assertEqual(sys.getrefcount(idx), start_count + 1) |
|
114 | self.assertEqual(sys.getrefcount(idx), start_count + 1) | |
81 | self.assertEqual(next(ait), 3) |
|
115 | self.assertEqual(next(ait), 3) | |
82 |
|
116 | |||
83 | # and decreases once the iterator is removed |
|
117 | # and decreases once the iterator is removed | |
84 | del ait |
|
118 | del ait | |
85 | self.assertEqual(sys.getrefcount(idx), start_count) |
|
119 | self.assertEqual(sys.getrefcount(idx), start_count) | |
86 |
|
120 | |||
87 | # and removing ref to the index after iterator init is no issue |
|
121 | # and removing ref to the index after iterator init is no issue | |
88 | ait = AncestorsIterator(idx, [3], 0, True) |
|
122 | ait = AncestorsIterator(idx, [3], 0, True) | |
89 | del idx |
|
123 | del idx | |
90 |
self.assertEqual( |
|
124 | self.assertEqual(list(ait), [3, 2, 1, 0]) | |
91 |
|
125 | |||
92 | def testgrapherror(self): |
|
126 | def testgrapherror(self): | |
93 | data = (data_non_inlined[:64 + 27] + |
|
127 | data = (data_non_inlined[:64 + 27] + | |
94 | b'\xf2' + |
|
128 | b'\xf2' + | |
95 | data_non_inlined[64 + 28:]) |
|
129 | data_non_inlined[64 + 28:]) | |
96 | idx = cparsers.parse_index2(data, False)[0] |
|
130 | idx = cparsers.parse_index2(data, False)[0] | |
97 | with self.assertRaises(rustext.GraphError) as arc: |
|
131 | with self.assertRaises(rustext.GraphError) as arc: | |
98 | AncestorsIterator(idx, [1], -1, False) |
|
132 | AncestorsIterator(idx, [1], -1, False) | |
99 | exc = arc.exception |
|
133 | exc = arc.exception | |
100 | self.assertIsInstance(exc, ValueError) |
|
134 | self.assertIsInstance(exc, ValueError) | |
101 | # rust-cpython issues appropriate str instances for Python 2 and 3 |
|
135 | # rust-cpython issues appropriate str instances for Python 2 and 3 | |
102 | self.assertEqual(exc.args, ('ParentOutOfRange', 1)) |
|
136 | self.assertEqual(exc.args, ('ParentOutOfRange', 1)) | |
103 |
|
137 | |||
104 |
|
138 | |||
105 | if __name__ == '__main__': |
|
139 | if __name__ == '__main__': | |
106 | import silenttestrunner |
|
140 | import silenttestrunner | |
107 | silenttestrunner.main(__name__) |
|
141 | silenttestrunner.main(__name__) |
General Comments 0
You need to be logged in to leave comments.
Login now