Show More
@@ -1,152 +1,122 b'' | |||||
1 | // dirstate.rs |
|
1 | // dirstate.rs | |
2 | // |
|
2 | // | |
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> |
|
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | |
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::dirstate` module provided by the |
|
8 | //! Bindings for the `hg::dirstate` module provided by the | |
9 | //! `hg-core` package. |
|
9 | //! `hg-core` package. | |
10 | //! |
|
10 | //! | |
11 | //! From Python, this will be seen as `mercurial.rustext.dirstate` |
|
11 | //! From Python, this will be seen as `mercurial.rustext.dirstate` | |
12 | mod copymap; |
|
12 | mod copymap; | |
13 | mod dirs_multiset; |
|
13 | mod dirs_multiset; | |
14 | mod dirstate_map; |
|
14 | mod dirstate_map; | |
15 | mod non_normal_entries; |
|
15 | mod non_normal_entries; | |
16 | mod status; |
|
16 | mod status; | |
17 | use crate::{ |
|
17 | use crate::{ | |
18 | dirstate::{ |
|
18 | dirstate::{ | |
19 | dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper, |
|
19 | dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper, | |
20 | }, |
|
20 | }, | |
21 | exceptions, |
|
21 | exceptions, | |
22 | }; |
|
22 | }; | |
23 | use cpython::{ |
|
23 | use cpython::{ | |
24 |
|
|
24 | PyBytes, PyDict, PyErr, PyList, PyModule, PyObject, PyResult, Python, | |
25 | PySequence, Python, |
|
|||
26 | }; |
|
25 | }; | |
27 | use hg::dirstate_tree::on_disk::V2_FORMAT_MARKER; |
|
26 | use hg::dirstate_tree::on_disk::V2_FORMAT_MARKER; | |
28 | use hg::{utils::hg_path::HgPathBuf, DirstateEntry, EntryState, StateMap}; |
|
27 | use hg::DirstateEntry; | |
29 | use libc::{c_char, c_int}; |
|
28 | use libc::{c_char, c_int}; | |
30 | use std::convert::TryFrom; |
|
|||
31 |
|
29 | |||
32 | // C code uses a custom `dirstate_tuple` type, checks in multiple instances |
|
30 | // C code uses a custom `dirstate_tuple` type, checks in multiple instances | |
33 | // for this type, and raises a Python `Exception` if the check does not pass. |
|
31 | // for this type, and raises a Python `Exception` if the check does not pass. | |
34 | // Because this type differs only in name from the regular Python tuple, it |
|
32 | // Because this type differs only in name from the regular Python tuple, it | |
35 | // would be a good idea in the near future to remove it entirely to allow |
|
33 | // would be a good idea in the near future to remove it entirely to allow | |
36 | // for a pure Python tuple of the same effective structure to be used, |
|
34 | // for a pure Python tuple of the same effective structure to be used, | |
37 | // rendering this type and the capsule below useless. |
|
35 | // rendering this type and the capsule below useless. | |
38 | py_capsule_fn!( |
|
36 | py_capsule_fn!( | |
39 | from mercurial.cext.parsers import make_dirstate_item_CAPI |
|
37 | from mercurial.cext.parsers import make_dirstate_item_CAPI | |
40 | as make_dirstate_item_capi |
|
38 | as make_dirstate_item_capi | |
41 | signature ( |
|
39 | signature ( | |
42 | state: c_char, |
|
40 | state: c_char, | |
43 | mode: c_int, |
|
41 | mode: c_int, | |
44 | size: c_int, |
|
42 | size: c_int, | |
45 | mtime: c_int, |
|
43 | mtime: c_int, | |
46 | ) -> *mut RawPyObject |
|
44 | ) -> *mut RawPyObject | |
47 | ); |
|
45 | ); | |
48 |
|
46 | |||
49 | pub fn make_dirstate_item( |
|
47 | pub fn make_dirstate_item( | |
50 | py: Python, |
|
48 | py: Python, | |
51 | entry: &DirstateEntry, |
|
49 | entry: &DirstateEntry, | |
52 | ) -> PyResult<PyObject> { |
|
50 | ) -> PyResult<PyObject> { | |
53 | let &DirstateEntry { |
|
51 | let &DirstateEntry { | |
54 | state, |
|
52 | state, | |
55 | mode, |
|
53 | mode, | |
56 | size, |
|
54 | size, | |
57 | mtime, |
|
55 | mtime, | |
58 | } = entry; |
|
56 | } = entry; | |
59 | // Explicitly go through u8 first, then cast to platform-specific `c_char` |
|
57 | // Explicitly go through u8 first, then cast to platform-specific `c_char` | |
60 | // because Into<u8> has a specific implementation while `as c_char` would |
|
58 | // because Into<u8> has a specific implementation while `as c_char` would | |
61 | // just do a naive enum cast. |
|
59 | // just do a naive enum cast. | |
62 | let state_code: u8 = state.into(); |
|
60 | let state_code: u8 = state.into(); | |
63 | make_dirstate_item_raw(py, state_code, mode, size, mtime) |
|
61 | make_dirstate_item_raw(py, state_code, mode, size, mtime) | |
64 | } |
|
62 | } | |
65 |
|
63 | |||
66 | pub fn make_dirstate_item_raw( |
|
64 | pub fn make_dirstate_item_raw( | |
67 | py: Python, |
|
65 | py: Python, | |
68 | state: u8, |
|
66 | state: u8, | |
69 | mode: i32, |
|
67 | mode: i32, | |
70 | size: i32, |
|
68 | size: i32, | |
71 | mtime: i32, |
|
69 | mtime: i32, | |
72 | ) -> PyResult<PyObject> { |
|
70 | ) -> PyResult<PyObject> { | |
73 | let make = make_dirstate_item_capi::retrieve(py)?; |
|
71 | let make = make_dirstate_item_capi::retrieve(py)?; | |
74 | let maybe_obj = unsafe { |
|
72 | let maybe_obj = unsafe { | |
75 | let ptr = make(state as c_char, mode, size, mtime); |
|
73 | let ptr = make(state as c_char, mode, size, mtime); | |
76 | PyObject::from_owned_ptr_opt(py, ptr) |
|
74 | PyObject::from_owned_ptr_opt(py, ptr) | |
77 | }; |
|
75 | }; | |
78 | maybe_obj.ok_or_else(|| PyErr::fetch(py)) |
|
76 | maybe_obj.ok_or_else(|| PyErr::fetch(py)) | |
79 | } |
|
77 | } | |
80 |
|
78 | |||
81 | pub fn extract_dirstate(py: Python, dmap: &PyDict) -> Result<StateMap, PyErr> { |
|
|||
82 | dmap.items(py) |
|
|||
83 | .iter() |
|
|||
84 | .map(|(filename, stats)| { |
|
|||
85 | let stats = stats.extract::<PySequence>(py)?; |
|
|||
86 | let state = stats.get_item(py, 0)?.extract::<PyBytes>(py)?; |
|
|||
87 | let state = |
|
|||
88 | EntryState::try_from(state.data(py)[0]).map_err(|e| { |
|
|||
89 | PyErr::new::<exc::ValueError, _>(py, e.to_string()) |
|
|||
90 | })?; |
|
|||
91 | let mode = stats.get_item(py, 1)?.extract(py)?; |
|
|||
92 | let size = stats.get_item(py, 2)?.extract(py)?; |
|
|||
93 | let mtime = stats.get_item(py, 3)?.extract(py)?; |
|
|||
94 | let filename = filename.extract::<PyBytes>(py)?; |
|
|||
95 | let filename = filename.data(py); |
|
|||
96 | Ok(( |
|
|||
97 | HgPathBuf::from(filename.to_owned()), |
|
|||
98 | DirstateEntry { |
|
|||
99 | state, |
|
|||
100 | mode, |
|
|||
101 | size, |
|
|||
102 | mtime, |
|
|||
103 | }, |
|
|||
104 | )) |
|
|||
105 | }) |
|
|||
106 | .collect() |
|
|||
107 | } |
|
|||
108 |
|
||||
109 | /// Create the module, with `__package__` given from parent |
|
79 | /// Create the module, with `__package__` given from parent | |
110 | pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> { |
|
80 | pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> { | |
111 | let dotted_name = &format!("{}.dirstate", package); |
|
81 | let dotted_name = &format!("{}.dirstate", package); | |
112 | let m = PyModule::new(py, dotted_name)?; |
|
82 | let m = PyModule::new(py, dotted_name)?; | |
113 |
|
83 | |||
114 | env_logger::init(); |
|
84 | env_logger::init(); | |
115 |
|
85 | |||
116 | m.add(py, "__package__", package)?; |
|
86 | m.add(py, "__package__", package)?; | |
117 | m.add(py, "__doc__", "Dirstate - Rust implementation")?; |
|
87 | m.add(py, "__doc__", "Dirstate - Rust implementation")?; | |
118 |
|
88 | |||
119 | m.add( |
|
89 | m.add( | |
120 | py, |
|
90 | py, | |
121 | "FallbackError", |
|
91 | "FallbackError", | |
122 | py.get_type::<exceptions::FallbackError>(), |
|
92 | py.get_type::<exceptions::FallbackError>(), | |
123 | )?; |
|
93 | )?; | |
124 | m.add_class::<Dirs>(py)?; |
|
94 | m.add_class::<Dirs>(py)?; | |
125 | m.add_class::<DirstateMap>(py)?; |
|
95 | m.add_class::<DirstateMap>(py)?; | |
126 | m.add(py, "V2_FORMAT_MARKER", PyBytes::new(py, V2_FORMAT_MARKER))?; |
|
96 | m.add(py, "V2_FORMAT_MARKER", PyBytes::new(py, V2_FORMAT_MARKER))?; | |
127 | m.add( |
|
97 | m.add( | |
128 | py, |
|
98 | py, | |
129 | "status", |
|
99 | "status", | |
130 | py_fn!( |
|
100 | py_fn!( | |
131 | py, |
|
101 | py, | |
132 | status_wrapper( |
|
102 | status_wrapper( | |
133 | dmap: DirstateMap, |
|
103 | dmap: DirstateMap, | |
134 | root_dir: PyObject, |
|
104 | root_dir: PyObject, | |
135 | matcher: PyObject, |
|
105 | matcher: PyObject, | |
136 | ignorefiles: PyList, |
|
106 | ignorefiles: PyList, | |
137 | check_exec: bool, |
|
107 | check_exec: bool, | |
138 | last_normal_time: i64, |
|
108 | last_normal_time: i64, | |
139 | list_clean: bool, |
|
109 | list_clean: bool, | |
140 | list_ignored: bool, |
|
110 | list_ignored: bool, | |
141 | list_unknown: bool, |
|
111 | list_unknown: bool, | |
142 | collect_traversed_dirs: bool |
|
112 | collect_traversed_dirs: bool | |
143 | ) |
|
113 | ) | |
144 | ), |
|
114 | ), | |
145 | )?; |
|
115 | )?; | |
146 |
|
116 | |||
147 | let sys = PyModule::import(py, "sys")?; |
|
117 | let sys = PyModule::import(py, "sys")?; | |
148 | let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?; |
|
118 | let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?; | |
149 | sys_modules.set_item(py, dotted_name, &m)?; |
|
119 | sys_modules.set_item(py, dotted_name, &m)?; | |
150 |
|
120 | |||
151 | Ok(m) |
|
121 | Ok(m) | |
152 | } |
|
122 | } |
@@ -1,134 +1,124 b'' | |||||
1 | // dirs_multiset.rs |
|
1 | // dirs_multiset.rs | |
2 | // |
|
2 | // | |
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> |
|
3 | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | |
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::dirstate::dirs_multiset` file provided by the |
|
8 | //! Bindings for the `hg::dirstate::dirs_multiset` file provided by the | |
9 | //! `hg-core` package. |
|
9 | //! `hg-core` package. | |
10 |
|
10 | |||
11 | use std::cell::RefCell; |
|
11 | use std::cell::RefCell; | |
12 |
|
12 | |||
13 | use cpython::{ |
|
13 | use cpython::{ | |
14 |
exc, ObjectProtocol |
|
14 | exc, ObjectProtocol, PyBytes, PyClone, PyDict, PyErr, PyObject, PyResult, | |
15 |
|
|
15 | Python, UnsafePyLeaked, | |
16 | }; |
|
16 | }; | |
17 |
|
17 | |||
18 | use crate::dirstate::extract_dirstate; |
|
|||
19 | use hg::{ |
|
18 | use hg::{ | |
20 | utils::hg_path::{HgPath, HgPathBuf}, |
|
19 | utils::hg_path::{HgPath, HgPathBuf}, | |
21 |
DirsMultiset, DirsMultisetIter, |
|
20 | DirsMultiset, DirsMultisetIter, DirstateMapError, | |
22 | }; |
|
21 | }; | |
23 |
|
22 | |||
24 | py_class!(pub class Dirs |py| { |
|
23 | py_class!(pub class Dirs |py| { | |
25 | @shared data inner: DirsMultiset; |
|
24 | @shared data inner: DirsMultiset; | |
26 |
|
25 | |||
27 | // `map` is either a `dict` or a flat iterator (usually a `set`, sometimes |
|
26 | // `map` is either a `dict` or a flat iterator (usually a `set`, sometimes | |
28 | // a `list`) |
|
27 | // a `list`) | |
29 | def __new__( |
|
28 | def __new__( | |
30 | _cls, |
|
29 | _cls, | |
31 | map: PyObject, |
|
30 | map: PyObject, | |
32 | only_tracked: Option<PyObject> = None |
|
|||
33 | ) -> PyResult<Self> { |
|
31 | ) -> PyResult<Self> { | |
34 | let only_tracked_b = if let Some(only_tracked) = only_tracked { |
|
32 | let inner = if map.cast_as::<PyDict>(py).is_ok() { | |
35 | only_tracked.extract::<PyBool>(py)?.is_true() |
|
33 | let err = "pathutil.dirs() with a dict should only be used by the Python dirstatemap \ | |
36 | } else { |
|
34 | and should not be used when Rust is enabled"; | |
37 | false |
|
35 | return Err(PyErr::new::<exc::TypeError, _>(py, err.to_string())) | |
38 | }; |
|
|||
39 | let inner = if let Ok(map) = map.cast_as::<PyDict>(py) { |
|
|||
40 | let dirstate = extract_dirstate(py, &map)?; |
|
|||
41 | let dirstate = dirstate.iter().map(|(k, v)| Ok((k, *v))); |
|
|||
42 | DirsMultiset::from_dirstate(dirstate, only_tracked_b) |
|
|||
43 | .map_err(|e: DirstateError| { |
|
|||
44 | PyErr::new::<exc::ValueError, _>(py, e.to_string()) |
|
|||
45 | })? |
|
|||
46 | } else { |
|
36 | } else { | |
47 | let map: Result<Vec<HgPathBuf>, PyErr> = map |
|
37 | let map: Result<Vec<HgPathBuf>, PyErr> = map | |
48 | .iter(py)? |
|
38 | .iter(py)? | |
49 | .map(|o| { |
|
39 | .map(|o| { | |
50 | Ok(HgPathBuf::from_bytes( |
|
40 | Ok(HgPathBuf::from_bytes( | |
51 | o?.extract::<PyBytes>(py)?.data(py), |
|
41 | o?.extract::<PyBytes>(py)?.data(py), | |
52 | )) |
|
42 | )) | |
53 | }) |
|
43 | }) | |
54 | .collect(); |
|
44 | .collect(); | |
55 | DirsMultiset::from_manifest(&map?) |
|
45 | DirsMultiset::from_manifest(&map?) | |
56 | .map_err(|e| { |
|
46 | .map_err(|e| { | |
57 | PyErr::new::<exc::ValueError, _>(py, e.to_string()) |
|
47 | PyErr::new::<exc::ValueError, _>(py, e.to_string()) | |
58 | })? |
|
48 | })? | |
59 | }; |
|
49 | }; | |
60 |
|
50 | |||
61 | Self::create_instance(py, inner) |
|
51 | Self::create_instance(py, inner) | |
62 | } |
|
52 | } | |
63 |
|
53 | |||
64 | def addpath(&self, path: PyObject) -> PyResult<PyObject> { |
|
54 | def addpath(&self, path: PyObject) -> PyResult<PyObject> { | |
65 | self.inner(py).borrow_mut().add_path( |
|
55 | self.inner(py).borrow_mut().add_path( | |
66 | HgPath::new(path.extract::<PyBytes>(py)?.data(py)), |
|
56 | HgPath::new(path.extract::<PyBytes>(py)?.data(py)), | |
67 | ).and(Ok(py.None())).or_else(|e| { |
|
57 | ).and(Ok(py.None())).or_else(|e| { | |
68 | match e { |
|
58 | match e { | |
69 | DirstateMapError::EmptyPath => { |
|
59 | DirstateMapError::EmptyPath => { | |
70 | Ok(py.None()) |
|
60 | Ok(py.None()) | |
71 | }, |
|
61 | }, | |
72 | e => { |
|
62 | e => { | |
73 | Err(PyErr::new::<exc::ValueError, _>( |
|
63 | Err(PyErr::new::<exc::ValueError, _>( | |
74 | py, |
|
64 | py, | |
75 | e.to_string(), |
|
65 | e.to_string(), | |
76 | )) |
|
66 | )) | |
77 | } |
|
67 | } | |
78 | } |
|
68 | } | |
79 | }) |
|
69 | }) | |
80 | } |
|
70 | } | |
81 |
|
71 | |||
82 | def delpath(&self, path: PyObject) -> PyResult<PyObject> { |
|
72 | def delpath(&self, path: PyObject) -> PyResult<PyObject> { | |
83 | self.inner(py).borrow_mut().delete_path( |
|
73 | self.inner(py).borrow_mut().delete_path( | |
84 | HgPath::new(path.extract::<PyBytes>(py)?.data(py)), |
|
74 | HgPath::new(path.extract::<PyBytes>(py)?.data(py)), | |
85 | ) |
|
75 | ) | |
86 | .and(Ok(py.None())) |
|
76 | .and(Ok(py.None())) | |
87 | .or_else(|e| { |
|
77 | .or_else(|e| { | |
88 | match e { |
|
78 | match e { | |
89 | DirstateMapError::EmptyPath => { |
|
79 | DirstateMapError::EmptyPath => { | |
90 | Ok(py.None()) |
|
80 | Ok(py.None()) | |
91 | }, |
|
81 | }, | |
92 | e => { |
|
82 | e => { | |
93 | Err(PyErr::new::<exc::ValueError, _>( |
|
83 | Err(PyErr::new::<exc::ValueError, _>( | |
94 | py, |
|
84 | py, | |
95 | e.to_string(), |
|
85 | e.to_string(), | |
96 | )) |
|
86 | )) | |
97 | } |
|
87 | } | |
98 | } |
|
88 | } | |
99 | }) |
|
89 | }) | |
100 | } |
|
90 | } | |
101 | def __iter__(&self) -> PyResult<DirsMultisetKeysIterator> { |
|
91 | def __iter__(&self) -> PyResult<DirsMultisetKeysIterator> { | |
102 | let leaked_ref = self.inner(py).leak_immutable(); |
|
92 | let leaked_ref = self.inner(py).leak_immutable(); | |
103 | DirsMultisetKeysIterator::from_inner( |
|
93 | DirsMultisetKeysIterator::from_inner( | |
104 | py, |
|
94 | py, | |
105 | unsafe { leaked_ref.map(py, |o| o.iter()) }, |
|
95 | unsafe { leaked_ref.map(py, |o| o.iter()) }, | |
106 | ) |
|
96 | ) | |
107 | } |
|
97 | } | |
108 |
|
98 | |||
109 | def __contains__(&self, item: PyObject) -> PyResult<bool> { |
|
99 | def __contains__(&self, item: PyObject) -> PyResult<bool> { | |
110 | Ok(self.inner(py).borrow().contains(HgPath::new( |
|
100 | Ok(self.inner(py).borrow().contains(HgPath::new( | |
111 | item.extract::<PyBytes>(py)?.data(py).as_ref(), |
|
101 | item.extract::<PyBytes>(py)?.data(py).as_ref(), | |
112 | ))) |
|
102 | ))) | |
113 | } |
|
103 | } | |
114 | }); |
|
104 | }); | |
115 |
|
105 | |||
116 | impl Dirs { |
|
106 | impl Dirs { | |
117 | pub fn from_inner(py: Python, d: DirsMultiset) -> PyResult<Self> { |
|
107 | pub fn from_inner(py: Python, d: DirsMultiset) -> PyResult<Self> { | |
118 | Self::create_instance(py, d) |
|
108 | Self::create_instance(py, d) | |
119 | } |
|
109 | } | |
120 |
|
110 | |||
121 | fn translate_key( |
|
111 | fn translate_key( | |
122 | py: Python, |
|
112 | py: Python, | |
123 | res: &HgPathBuf, |
|
113 | res: &HgPathBuf, | |
124 | ) -> PyResult<Option<PyBytes>> { |
|
114 | ) -> PyResult<Option<PyBytes>> { | |
125 | Ok(Some(PyBytes::new(py, res.as_bytes()))) |
|
115 | Ok(Some(PyBytes::new(py, res.as_bytes()))) | |
126 | } |
|
116 | } | |
127 | } |
|
117 | } | |
128 |
|
118 | |||
129 | py_shared_iterator!( |
|
119 | py_shared_iterator!( | |
130 | DirsMultisetKeysIterator, |
|
120 | DirsMultisetKeysIterator, | |
131 | UnsafePyLeaked<DirsMultisetIter<'static>>, |
|
121 | UnsafePyLeaked<DirsMultisetIter<'static>>, | |
132 | Dirs::translate_key, |
|
122 | Dirs::translate_key, | |
133 | Option<PyBytes> |
|
123 | Option<PyBytes> | |
134 | ); |
|
124 | ); |
@@ -1,27 +1,27 b'' | |||||
1 | from __future__ import absolute_import |
|
1 | from __future__ import absolute_import | |
2 |
|
2 | |||
3 | import unittest |
|
3 | import unittest | |
4 |
|
4 | |||
5 | import silenttestrunner |
|
5 | import silenttestrunner | |
6 |
|
6 | |||
7 | from mercurial import pathutil |
|
7 | from mercurial import pathutil | |
8 |
|
8 | |||
9 |
|
9 | |||
10 | class dirstests(unittest.TestCase): |
|
10 | class dirstests(unittest.TestCase): | |
11 | def testdirs(self): |
|
11 | def testdirs(self): | |
12 | for case, want in [ |
|
12 | for case, want in [ | |
13 | (b'a/a/a', [b'a', b'a/a', b'']), |
|
13 | (b'a/a/a', [b'a', b'a/a', b'']), | |
14 | (b'alpha/beta/gamma', [b'', b'alpha', b'alpha/beta']), |
|
14 | (b'alpha/beta/gamma', [b'', b'alpha', b'alpha/beta']), | |
15 | ]: |
|
15 | ]: | |
16 |
d = pathutil.dirs( |
|
16 | d = pathutil.dirs([]) | |
17 | d.addpath(case) |
|
17 | d.addpath(case) | |
18 | self.assertEqual(sorted(d), sorted(want)) |
|
18 | self.assertEqual(sorted(d), sorted(want)) | |
19 |
|
19 | |||
20 | def testinvalid(self): |
|
20 | def testinvalid(self): | |
21 | with self.assertRaises(ValueError): |
|
21 | with self.assertRaises(ValueError): | |
22 |
d = pathutil.dirs( |
|
22 | d = pathutil.dirs([]) | |
23 | d.addpath(b'a//b') |
|
23 | d.addpath(b'a//b') | |
24 |
|
24 | |||
25 |
|
25 | |||
26 | if __name__ == '__main__': |
|
26 | if __name__ == '__main__': | |
27 | silenttestrunner.main(__name__) |
|
27 | silenttestrunner.main(__name__) |
General Comments 0
You need to be logged in to leave comments.
Login now