cleepcli 1.44.0__tar.gz → 1.44.2__tar.gz
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.
- {cleepcli-1.44.0/cleepcli.egg-info → cleepcli-1.44.2}/PKG-INFO +1 -1
- {cleepcli-1.44.0 → cleepcli-1.44.2}/bin/ccli +15 -4
- {cleepcli-1.44.0 → cleepcli-1.44.2}/bin/cleep-cli +15 -4
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/file.py +61 -34
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/version.py +1 -1
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/watch.py +141 -49
- {cleepcli-1.44.0 → cleepcli-1.44.2/cleepcli.egg-info}/PKG-INFO +1 -1
- cleepcli-1.44.2/tests/test_watch.py +314 -0
- cleepcli-1.44.0/tests/test_watch.py +0 -208
- {cleepcli-1.44.0 → cleepcli-1.44.2}/LICENSE +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/MANIFEST.in +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/README.md +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/__init__.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/check.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/ci.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/cleepapi.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/config.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/console.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/docs.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/git.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/module.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/package.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/test.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli/tools.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/SOURCES.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/dependency_links.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/requires.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/cleepcli.egg-info/top_level.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/pyproject.toml +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/requirements.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/setup.cfg +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/setup.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.2}/tests/test_console.py +0 -0
|
@@ -291,11 +291,22 @@ def modssync(run_scripts):
|
|
|
291
291
|
f = File()
|
|
292
292
|
|
|
293
293
|
for module in config.MODULES_REPO_URL.keys():
|
|
294
|
-
res = f.module_sync(module)
|
|
295
|
-
if not res:
|
|
296
|
-
sys.exit(1)
|
|
297
294
|
if run_scripts:
|
|
298
|
-
|
|
295
|
+
# Same order as package install: scripts + preinst, then files, then postinst
|
|
296
|
+
res = f.module_sync(module, backend=False, frontend=False, scripts=True)
|
|
297
|
+
if not res:
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
res = f.module_run_install_script(module, "preinst.sh")
|
|
300
|
+
if not res:
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
res = f.module_sync(module, backend=True, frontend=True, scripts=True)
|
|
303
|
+
if not res:
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
res = f.module_run_install_script(module, "postinst.sh")
|
|
306
|
+
if not res:
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
else:
|
|
309
|
+
res = f.module_sync(module)
|
|
299
310
|
if not res:
|
|
300
311
|
sys.exit(1)
|
|
301
312
|
|
|
@@ -291,11 +291,22 @@ def modssync(run_scripts):
|
|
|
291
291
|
f = File()
|
|
292
292
|
|
|
293
293
|
for module in config.MODULES_REPO_URL.keys():
|
|
294
|
-
res = f.module_sync(module)
|
|
295
|
-
if not res:
|
|
296
|
-
sys.exit(1)
|
|
297
294
|
if run_scripts:
|
|
298
|
-
|
|
295
|
+
# Same order as package install: scripts + preinst, then files, then postinst
|
|
296
|
+
res = f.module_sync(module, backend=False, frontend=False, scripts=True)
|
|
297
|
+
if not res:
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
res = f.module_run_install_script(module, "preinst.sh")
|
|
300
|
+
if not res:
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
res = f.module_sync(module, backend=True, frontend=True, scripts=True)
|
|
303
|
+
if not res:
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
res = f.module_run_install_script(module, "postinst.sh")
|
|
306
|
+
if not res:
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
else:
|
|
309
|
+
res = f.module_sync(module)
|
|
299
310
|
if not res:
|
|
300
311
|
sys.exit(1)
|
|
301
312
|
|
|
@@ -31,12 +31,19 @@ class File():
|
|
|
31
31
|
'MEDIA_DST': config.MEDIA_DST,
|
|
32
32
|
'CONFIG_DIR': config.CONFIG_DIR,
|
|
33
33
|
}
|
|
34
|
+
variables['MODULES_DST'] = config.MODULES_DST
|
|
34
35
|
cmd = """
|
|
35
36
|
set -e
|
|
36
37
|
mkdir -p "%(HTML_DST)s/"
|
|
37
|
-
mkdir -p "%(CORE_DST)s
|
|
38
|
+
mkdir -p "%(CORE_DST)s"
|
|
39
|
+
mkdir -p "%(MODULES_DST)s"
|
|
38
40
|
mkdir -p "%(MEDIA_DST)s/modules"
|
|
39
41
|
mkdir -p "%(CONFIG_DIR)s"
|
|
42
|
+
# Python imports cleep.modules from CORE_DST/modules; it must point at the live apps tree
|
|
43
|
+
if [ -e "%(CORE_DST)s/modules" ] && [ ! -L "%(CORE_DST)s/modules" ]; then
|
|
44
|
+
rm -rf "%(CORE_DST)s/modules"
|
|
45
|
+
fi
|
|
46
|
+
ln -sfn "%(MODULES_DST)s" "%(CORE_DST)s/modules"
|
|
40
47
|
rsync -av "%(REPO_DIR)s/cleep/" "%(CORE_DST)s/" --exclude "/tests/" --exclude "modules" --exclude "*__pycache__*" --delete --exclude "*.pyc" --keep-dirlinks
|
|
41
48
|
rsync -av "%(REPO_DIR)s/html/" "%(HTML_DST)s/" --delete --exclude "js/modules/" --exclude "*node_modules*"
|
|
42
49
|
rsync -av "%(REPO_DIR)s/bin/cleep" "%(BIN_DST)s/cleep"
|
|
@@ -53,16 +60,19 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
|
|
|
53
60
|
|
|
54
61
|
return True
|
|
55
62
|
|
|
56
|
-
def module_sync(self, module_name):
|
|
57
|
-
"""
|
|
63
|
+
def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
|
|
64
|
+
"""
|
|
58
65
|
Copy specified module content to valid execution location
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
module_name (str): module folder name under MODULES_SRC
|
|
69
|
+
backend (bool): sync backend/ when True
|
|
70
|
+
frontend (bool): sync frontend/ when True
|
|
71
|
+
scripts (bool): sync scripts/ when True
|
|
59
72
|
"""
|
|
60
73
|
if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
|
|
61
74
|
self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
|
|
62
75
|
return False
|
|
63
|
-
|
|
64
|
-
if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
|
|
65
|
-
os.makedirs(os.path.join(config.MODULES_DST, module_name))
|
|
66
76
|
|
|
67
77
|
mod_backend_src = os.path.join(config.MODULES_SRC, module_name, 'backend/')
|
|
68
78
|
mod_backend_dst = os.path.join(config.MODULES_DST, module_name)
|
|
@@ -71,26 +81,34 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
|
|
|
71
81
|
mod_scripts_src = os.path.join(config.MODULES_SRC, module_name, 'scripts/')
|
|
72
82
|
mod_scripts_dst = os.path.join(config.MODULES_SCRIPTS_DST, module_name)
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
parts = []
|
|
85
|
+
if backend:
|
|
86
|
+
parts.append("""
|
|
77
87
|
if [ -d "%(BACKEND_SRC)s" ]; then
|
|
78
88
|
/bin/mkdir -p "%(BACKEND_DST)s"
|
|
79
89
|
rsync -av "%(BACKEND_SRC)s" "%(BACKEND_DST)s" --delete --exclude "*.pyc" --exclude "*__pycache__*"
|
|
80
90
|
fi
|
|
91
|
+
""" % {'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst})
|
|
92
|
+
if frontend:
|
|
93
|
+
parts.append("""
|
|
81
94
|
if [ -d "%(FRONTEND_SRC)s" ]; then
|
|
82
95
|
/bin/mkdir -p "%(FRONTEND_DST)s"
|
|
83
96
|
rsync -av "%(FRONTEND_SRC)s" "%(FRONTEND_DST)s" --delete --exclude "*node_modules*"
|
|
84
97
|
fi
|
|
98
|
+
""" % {'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst})
|
|
99
|
+
if scripts:
|
|
100
|
+
parts.append("""
|
|
85
101
|
if [ -d "%(SCRIPTS_SRC)s" ]; then
|
|
86
102
|
/bin/mkdir -p "%(SCRIPTS_DST)s"
|
|
87
103
|
rsync -av "%(SCRIPTS_SRC)s" "%(SCRIPTS_DST)s" --delete --exclude "*__pycache__*"
|
|
88
104
|
fi
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
""" % {'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst})
|
|
106
|
+
|
|
107
|
+
if not parts:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
c = Console()
|
|
111
|
+
cmd = "set -e\n" + "\n".join(parts)
|
|
94
112
|
self.logger.debug('Modsync cmd: %s' % cmd)
|
|
95
113
|
resp = c.command(cmd, 120)
|
|
96
114
|
|
|
@@ -103,30 +121,39 @@ fi
|
|
|
103
121
|
|
|
104
122
|
return True
|
|
105
123
|
|
|
106
|
-
def
|
|
124
|
+
def module_run_install_script(self, module_name, script_name):
|
|
107
125
|
"""
|
|
108
|
-
Run module installation scripts
|
|
126
|
+
Run one module installation script from the synced scripts directory
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
module_name (str): module name
|
|
130
|
+
script_name (str): script filename (preinst.sh, postinst.sh, ...)
|
|
109
131
|
"""
|
|
110
132
|
if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
|
|
111
133
|
self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
|
|
112
134
|
return False
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if resp["killed"] or resp["returncode"] != 0:
|
|
129
|
-
self.logger.error('Error running %s [%s]: %s', postinst_script, resp["returncode"], ''.join(resp["stderr"]))
|
|
130
|
-
return False
|
|
131
|
-
|
|
136
|
+
script_path = os.path.join(config.MODULES_SCRIPTS_DST, module_name, script_name)
|
|
137
|
+
if not os.path.exists(script_path):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
self.logger.info('Running %s', script_path)
|
|
141
|
+
resp = Console().command(script_path, timeout=300)
|
|
142
|
+
if resp["killed"] or resp["returncode"] != 0:
|
|
143
|
+
self.logger.error(
|
|
144
|
+
'Error running %s [%s]: %s',
|
|
145
|
+
script_path,
|
|
146
|
+
resp["returncode"],
|
|
147
|
+
''.join(resp["stderr"]),
|
|
148
|
+
)
|
|
149
|
+
return False
|
|
132
150
|
return True
|
|
151
|
+
|
|
152
|
+
def module_run_install_scripts(self, module_name):
|
|
153
|
+
"""
|
|
154
|
+
Run module installation scripts (preinst then postinst)
|
|
155
|
+
"""
|
|
156
|
+
return (
|
|
157
|
+
self.module_run_install_script(module_name, "preinst.sh")
|
|
158
|
+
and self.module_run_install_script(module_name, "postinst.sh")
|
|
159
|
+
)
|
|
@@ -11,6 +11,7 @@ from watchdog.observers import Observer
|
|
|
11
11
|
from watchdog.events import (
|
|
12
12
|
FileSystemEventHandler,
|
|
13
13
|
EVENT_TYPE_CREATED,
|
|
14
|
+
EVENT_TYPE_DELETED,
|
|
14
15
|
EVENT_TYPE_MODIFIED,
|
|
15
16
|
EVENT_TYPE_MOVED,
|
|
16
17
|
)
|
|
@@ -18,7 +19,12 @@ import watchdog.events as events
|
|
|
18
19
|
from .cleepapi import CleepApi
|
|
19
20
|
from .file import File
|
|
20
21
|
|
|
21
|
-
_WATCHED_EVENT_TYPES = (
|
|
22
|
+
_WATCHED_EVENT_TYPES = (
|
|
23
|
+
EVENT_TYPE_CREATED,
|
|
24
|
+
EVENT_TYPE_DELETED,
|
|
25
|
+
EVENT_TYPE_MODIFIED,
|
|
26
|
+
EVENT_TYPE_MOVED,
|
|
27
|
+
)
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
def _is_under(path, root):
|
|
@@ -27,20 +33,42 @@ def _is_under(path, root):
|
|
|
27
33
|
return path == root or path.startswith(root + os.sep)
|
|
28
34
|
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
# One observer.schedule() = one inotify instance. Keep that count low
|
|
37
|
+
# (kernel default is often 128) by using recursive watches on a few roots.
|
|
38
|
+
MODULE_CONTENT_DIRS = ("backend", "frontend", "scripts")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def iter_watch_roots():
|
|
31
42
|
"""
|
|
32
|
-
Yield
|
|
43
|
+
Yield (path, recursive) pairs to schedule on the observer.
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
Core/html/bin are watched recursively. Modules watch only backend,
|
|
46
|
+
frontend and scripts so .git under each app is never registered.
|
|
36
47
|
"""
|
|
37
|
-
|
|
48
|
+
for root in (config.CORE_SRC, config.HTML_SRC, config.BIN_SRC):
|
|
49
|
+
if os.path.isdir(root):
|
|
50
|
+
yield root, True
|
|
51
|
+
|
|
52
|
+
if not os.path.isdir(config.MODULES_SRC):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
yield config.MODULES_SRC, False
|
|
56
|
+
try:
|
|
57
|
+
names = sorted(os.listdir(config.MODULES_SRC))
|
|
58
|
+
except OSError:
|
|
38
59
|
return
|
|
39
|
-
for
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
for name in names:
|
|
61
|
+
if name in CleepHandler.REJECTED_DIRS:
|
|
62
|
+
continue
|
|
63
|
+
module_path = os.path.join(config.MODULES_SRC, name)
|
|
64
|
+
if not os.path.isdir(module_path):
|
|
65
|
+
continue
|
|
66
|
+
# Non-recursive: see backend/frontend/scripts appear without watching .git
|
|
67
|
+
yield module_path, False
|
|
68
|
+
for sub in MODULE_CONTENT_DIRS:
|
|
69
|
+
content = os.path.join(module_path, sub)
|
|
70
|
+
if os.path.isdir(content):
|
|
71
|
+
yield content, True
|
|
44
72
|
|
|
45
73
|
|
|
46
74
|
class _Batch:
|
|
@@ -96,9 +124,12 @@ class ActionsExecutor(Thread):
|
|
|
96
124
|
"""
|
|
97
125
|
self.running = False
|
|
98
126
|
|
|
99
|
-
def note_change(self, module=None, core=False, frontend=False):
|
|
127
|
+
def note_change(self, module=None, core=False, frontend=False, backend=False):
|
|
100
128
|
"""
|
|
101
129
|
Remember a source change. The sync runs once writes stay quiet.
|
|
130
|
+
|
|
131
|
+
Scripts-only edits set neither frontend nor backend: files are synced
|
|
132
|
+
and Cleep is left running (install scripts are not re-run on watch).
|
|
102
133
|
"""
|
|
103
134
|
with self._lock:
|
|
104
135
|
if module:
|
|
@@ -107,7 +138,7 @@ class ActionsExecutor(Thread):
|
|
|
107
138
|
self._batch.core = True
|
|
108
139
|
if frontend:
|
|
109
140
|
self._batch.frontend = True
|
|
110
|
-
|
|
141
|
+
if backend:
|
|
111
142
|
self._batch.backend = True
|
|
112
143
|
self._batch.last = time.monotonic()
|
|
113
144
|
|
|
@@ -200,24 +231,54 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
200
231
|
return getattr(event, "dest_path", None) or event.src_path
|
|
201
232
|
return None
|
|
202
233
|
|
|
203
|
-
def
|
|
234
|
+
def schedule_all(self):
|
|
204
235
|
"""
|
|
205
|
-
|
|
236
|
+
Install the few recursive watches needed for core and module sources.
|
|
206
237
|
"""
|
|
207
|
-
for
|
|
208
|
-
self.
|
|
238
|
+
for path, recursive in iter_watch_roots():
|
|
239
|
+
self._schedule(path, recursive=recursive)
|
|
209
240
|
|
|
210
|
-
def
|
|
241
|
+
def _schedule(self, path, recursive=True):
|
|
211
242
|
"""
|
|
212
|
-
Watch
|
|
243
|
+
Watch path once. Rejected names and duplicates are skipped.
|
|
213
244
|
"""
|
|
214
245
|
if self.observer is None or not path or self._rejected_path(path):
|
|
215
|
-
return
|
|
246
|
+
return False
|
|
216
247
|
real = os.path.abspath(path)
|
|
217
248
|
if real in self._watched or not os.path.isdir(real):
|
|
218
|
-
return
|
|
219
|
-
self.observer.schedule(self, real, recursive=
|
|
249
|
+
return False
|
|
250
|
+
self.observer.schedule(self, real, recursive=recursive)
|
|
220
251
|
self._watched.add(real)
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
def _schedule_module(self, module_path):
|
|
255
|
+
"""
|
|
256
|
+
Watch a module root (non-recursive) and its content dirs. Never watches .git.
|
|
257
|
+
"""
|
|
258
|
+
if not module_path or not os.path.isdir(module_path):
|
|
259
|
+
return
|
|
260
|
+
if os.path.basename(module_path) in self.REJECTED_DIRS:
|
|
261
|
+
return
|
|
262
|
+
self._schedule(module_path, recursive=False)
|
|
263
|
+
for sub in MODULE_CONTENT_DIRS:
|
|
264
|
+
self._schedule(os.path.join(module_path, sub), recursive=True)
|
|
265
|
+
|
|
266
|
+
def _maybe_schedule_new_path(self, path):
|
|
267
|
+
"""
|
|
268
|
+
Add watches when a new module or module content dir appears.
|
|
269
|
+
|
|
270
|
+
Core/html/bin are already recursive, so new dirs there need no schedule.
|
|
271
|
+
"""
|
|
272
|
+
if not path or self._rejected_path(path):
|
|
273
|
+
return
|
|
274
|
+
parent = os.path.dirname(path)
|
|
275
|
+
name = os.path.basename(path)
|
|
276
|
+
modules_root = os.path.abspath(config.MODULES_SRC)
|
|
277
|
+
if os.path.abspath(parent) == modules_root:
|
|
278
|
+
self._schedule_module(path)
|
|
279
|
+
return
|
|
280
|
+
if name in MODULE_CONTENT_DIRS and os.path.abspath(os.path.dirname(parent)) == modules_root:
|
|
281
|
+
self._schedule(path, recursive=True)
|
|
221
282
|
|
|
222
283
|
def _module_name_from_path(self, path):
|
|
223
284
|
prefix = config.MODULES_SRC.rstrip(os.sep) + os.sep
|
|
@@ -313,7 +374,7 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
313
374
|
Returns:
|
|
314
375
|
module name (string) or None if nothing found
|
|
315
376
|
"""
|
|
316
|
-
pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)
|
|
377
|
+
pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)(?:/.*)?$" % (
|
|
317
378
|
re.escape(config.MODULES_SRC)
|
|
318
379
|
)
|
|
319
380
|
# self.logger.debug('Module pattern: %s' % pattern)
|
|
@@ -364,7 +425,7 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
364
425
|
Returns:
|
|
365
426
|
True if change on frontend, False otherwise
|
|
366
427
|
"""
|
|
367
|
-
pattern = r"^(%s/|%s/(?:.*?)/frontend
|
|
428
|
+
pattern = r"^(%s/|%s/(?:.*?)/frontend(?:/)?).*$" % (
|
|
368
429
|
re.escape(config.HTML_SRC),
|
|
369
430
|
re.escape(config.MODULES_SRC),
|
|
370
431
|
)
|
|
@@ -376,16 +437,31 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
376
437
|
|
|
377
438
|
return False
|
|
378
439
|
|
|
440
|
+
def __change_on_scripts(self, event):
|
|
441
|
+
"""
|
|
442
|
+
Detect change on module install scripts
|
|
443
|
+
"""
|
|
444
|
+
pattern = r"^%s/(?:.*?)/scripts(?:/.*)?$" % (re.escape(config.MODULES_SRC))
|
|
445
|
+
path = self._event_path(event)
|
|
446
|
+
return bool(path and re.search(pattern, path))
|
|
447
|
+
|
|
379
448
|
def _queue_change(self, event):
|
|
380
449
|
frontend = self.__change_on_frontend(event)
|
|
450
|
+
scripts = self.__change_on_scripts(event)
|
|
381
451
|
module = self.__change_on_module(event)
|
|
382
452
|
core = module is None and self.__change_on_core(event)
|
|
453
|
+
# Scripts are synced for the next install; they do not reload Cleep.
|
|
454
|
+
backend = not frontend and not scripts
|
|
383
455
|
if module:
|
|
384
456
|
self.logger.debug(' Change on module "%s"' % module)
|
|
385
|
-
self.actions_executor.note_change(
|
|
457
|
+
self.actions_executor.note_change(
|
|
458
|
+
module=module, frontend=frontend, backend=backend
|
|
459
|
+
)
|
|
386
460
|
elif core:
|
|
387
461
|
self.logger.debug(" Change on core")
|
|
388
|
-
self.actions_executor.note_change(
|
|
462
|
+
self.actions_executor.note_change(
|
|
463
|
+
core=True, frontend=frontend, backend=backend
|
|
464
|
+
)
|
|
389
465
|
else:
|
|
390
466
|
self.logger.debug(" Dropped due to useless change")
|
|
391
467
|
|
|
@@ -394,22 +470,25 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
394
470
|
self.logger.debug("Event: %s" % event)
|
|
395
471
|
created = self._created_directory(event)
|
|
396
472
|
if created:
|
|
397
|
-
self.schedule_tree(created)
|
|
398
473
|
if self._rejected_path(created):
|
|
399
474
|
self.logger.debug(" Filtered: directory")
|
|
400
475
|
return
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
self.actions_executor.note_change(module=module, frontend=frontend)
|
|
476
|
+
self._maybe_schedule_new_path(created)
|
|
477
|
+
# New module root: only schedule watches; wait for content files
|
|
478
|
+
modules_root = os.path.abspath(config.MODULES_SRC)
|
|
479
|
+
if os.path.abspath(os.path.dirname(created)) == modules_root:
|
|
406
480
|
return
|
|
407
|
-
if
|
|
481
|
+
if self.__change_on_module(event):
|
|
482
|
+
self._queue_change(event)
|
|
483
|
+
elif _is_under(created, config.CORE_SRC) or _is_under(
|
|
408
484
|
created, config.HTML_SRC
|
|
409
485
|
) or _is_under(created, config.BIN_SRC):
|
|
410
486
|
frontend = self.__change_on_frontend(event)
|
|
411
|
-
self.
|
|
412
|
-
|
|
487
|
+
self.actions_executor.note_change(
|
|
488
|
+
core=True,
|
|
489
|
+
frontend=frontend,
|
|
490
|
+
backend=not frontend,
|
|
491
|
+
)
|
|
413
492
|
return
|
|
414
493
|
|
|
415
494
|
if self.__is_event_dropped(event):
|
|
@@ -451,25 +530,38 @@ class CleepWatchdog:
|
|
|
451
530
|
event_handler = CleepHandler(self.actions_executor)
|
|
452
531
|
observer = Observer()
|
|
453
532
|
event_handler.observer = observer
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if not
|
|
533
|
+
try:
|
|
534
|
+
event_handler.schedule_all()
|
|
535
|
+
except OSError as error:
|
|
536
|
+
self.logger.error(
|
|
537
|
+
"Unable to start filesystem watches (%s). "
|
|
538
|
+
"Check fs.inotify.max_user_instances / max_user_watches",
|
|
539
|
+
error,
|
|
540
|
+
)
|
|
541
|
+
self.actions_executor.stop()
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
if not event_handler._watched:
|
|
466
545
|
self.logger.error(
|
|
467
546
|
'Please get Cleep sources using "cleep-cli getcore" command'
|
|
468
547
|
)
|
|
548
|
+
self.actions_executor.stop()
|
|
469
549
|
return False
|
|
470
550
|
|
|
471
|
-
self.logger.info(
|
|
472
|
-
|
|
551
|
+
self.logger.info(
|
|
552
|
+
'Cleep watchdog is running on "%s" (%d watches, CTRL-C to stop)'
|
|
553
|
+
% (path, len(event_handler._watched))
|
|
554
|
+
)
|
|
555
|
+
try:
|
|
556
|
+
observer.start()
|
|
557
|
+
except OSError as error:
|
|
558
|
+
self.logger.error(
|
|
559
|
+
"Unable to start filesystem watches (%s). "
|
|
560
|
+
"Check fs.inotify.max_user_instances / max_user_watches",
|
|
561
|
+
error,
|
|
562
|
+
)
|
|
563
|
+
self.actions_executor.stop()
|
|
564
|
+
return False
|
|
473
565
|
try:
|
|
474
566
|
while True:
|
|
475
567
|
time.sleep(0.25)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import Mock, patch
|
|
10
|
+
|
|
11
|
+
from watchdog.events import (
|
|
12
|
+
DirCreatedEvent,
|
|
13
|
+
FileDeletedEvent,
|
|
14
|
+
FileModifiedEvent,
|
|
15
|
+
FileMovedEvent,
|
|
16
|
+
)
|
|
17
|
+
from watchdog.observers import Observer
|
|
18
|
+
|
|
19
|
+
from cleepcli import config
|
|
20
|
+
from cleepcli.watch import ActionsExecutor, CleepHandler, iter_watch_roots
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IterWatchRootsTests(unittest.TestCase):
|
|
24
|
+
def test_skips_module_git_and_watches_content_dirs(self):
|
|
25
|
+
repo = tempfile.mkdtemp()
|
|
26
|
+
try:
|
|
27
|
+
core = os.path.join(repo, "cleep")
|
|
28
|
+
html = os.path.join(repo, "html")
|
|
29
|
+
bin_dir = os.path.join(repo, "bin")
|
|
30
|
+
modules = os.path.join(repo, "modules")
|
|
31
|
+
os.makedirs(os.path.join(core, "libs"))
|
|
32
|
+
os.makedirs(html)
|
|
33
|
+
os.makedirs(bin_dir)
|
|
34
|
+
os.makedirs(os.path.join(modules, "audio", ".git", "objects", "ab"))
|
|
35
|
+
os.makedirs(os.path.join(modules, "audio", "backend"))
|
|
36
|
+
os.makedirs(os.path.join(modules, "audio", "frontend"))
|
|
37
|
+
os.makedirs(os.path.join(modules, "audio", "tests"))
|
|
38
|
+
|
|
39
|
+
with patch.object(config, "CORE_SRC", core), patch.object(
|
|
40
|
+
config, "HTML_SRC", html
|
|
41
|
+
), patch.object(config, "BIN_SRC", bin_dir), patch.object(
|
|
42
|
+
config, "MODULES_SRC", modules
|
|
43
|
+
):
|
|
44
|
+
roots = list(iter_watch_roots())
|
|
45
|
+
finally:
|
|
46
|
+
shutil.rmtree(repo)
|
|
47
|
+
|
|
48
|
+
paths = {path for path, _recursive in roots}
|
|
49
|
+
recursive = {path: recursive for path, recursive in roots}
|
|
50
|
+
self.assertIn(core, paths)
|
|
51
|
+
self.assertTrue(recursive[core])
|
|
52
|
+
self.assertIn(html, paths)
|
|
53
|
+
self.assertIn(bin_dir, paths)
|
|
54
|
+
self.assertIn(modules, paths)
|
|
55
|
+
self.assertFalse(recursive[modules])
|
|
56
|
+
self.assertIn(os.path.join(modules, "audio"), paths)
|
|
57
|
+
self.assertFalse(recursive[os.path.join(modules, "audio")])
|
|
58
|
+
self.assertIn(os.path.join(modules, "audio", "backend"), paths)
|
|
59
|
+
self.assertIn(os.path.join(modules, "audio", "frontend"), paths)
|
|
60
|
+
self.assertTrue(recursive[os.path.join(modules, "audio", "backend")])
|
|
61
|
+
self.assertFalse(any(".git" in path.split(os.sep) for path in paths))
|
|
62
|
+
self.assertFalse(any(path.endswith(os.path.join("audio", "tests")) for path in paths))
|
|
63
|
+
# 3 core trees + modules root + module root + 2 content dirs
|
|
64
|
+
self.assertEqual(len(roots), 7)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HandlerTests(unittest.TestCase):
|
|
68
|
+
def setUp(self):
|
|
69
|
+
self.executor = Mock()
|
|
70
|
+
self.handler = CleepHandler(self.executor)
|
|
71
|
+
|
|
72
|
+
def test_git_file_is_ignored(self):
|
|
73
|
+
path = os.path.join(
|
|
74
|
+
config.MODULES_SRC, "cleepbus.tangb", ".git", "objects", "ab", "deadbeef"
|
|
75
|
+
)
|
|
76
|
+
self.handler.on_any_event(FileModifiedEvent(path))
|
|
77
|
+
self.executor.note_change.assert_not_called()
|
|
78
|
+
|
|
79
|
+
def test_tests_file_is_ignored(self):
|
|
80
|
+
path = os.path.join(config.CORE_SRC, "libs", "tests", "test_rpcserver.py")
|
|
81
|
+
self.handler.on_any_event(FileModifiedEvent(path))
|
|
82
|
+
self.executor.note_change.assert_not_called()
|
|
83
|
+
|
|
84
|
+
def test_core_file_is_queued(self):
|
|
85
|
+
path = os.path.join(config.CORE_SRC, "rpcserver.py")
|
|
86
|
+
self.handler.on_any_event(FileModifiedEvent(path))
|
|
87
|
+
self.executor.note_change.assert_called_once_with(
|
|
88
|
+
core=True, frontend=False, backend=True
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def test_atomic_save_move_is_queued(self):
|
|
92
|
+
src = os.path.join(config.CORE_SRC, ".rpcserver.py.tmp")
|
|
93
|
+
dest = os.path.join(config.CORE_SRC, "rpcserver.py")
|
|
94
|
+
self.handler.on_any_event(FileMovedEvent(src, dest))
|
|
95
|
+
self.executor.note_change.assert_called_once_with(
|
|
96
|
+
core=True, frontend=False, backend=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def test_module_file_delete_is_queued(self):
|
|
100
|
+
path = os.path.join(config.MODULES_SRC, "audio", "backend", "audio.py")
|
|
101
|
+
self.handler.on_any_event(FileDeletedEvent(path))
|
|
102
|
+
self.executor.note_change.assert_called_once_with(
|
|
103
|
+
module="audio", frontend=False, backend=True
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def test_module_scripts_do_not_restart_backend(self):
|
|
107
|
+
path = os.path.join(config.MODULES_SRC, "audio", "scripts", "postinst.sh")
|
|
108
|
+
self.handler.on_any_event(FileModifiedEvent(path))
|
|
109
|
+
self.executor.note_change.assert_called_once_with(
|
|
110
|
+
module="audio", frontend=False, backend=False
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def test_module_frontend_is_queued(self):
|
|
114
|
+
path = os.path.join(config.MODULES_SRC, "audio", "frontend", "desc.json")
|
|
115
|
+
self.handler.on_any_event(FileModifiedEvent(path))
|
|
116
|
+
self.executor.note_change.assert_called_once_with(
|
|
117
|
+
module="audio", frontend=True, backend=False
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def test_created_git_directory_is_not_watched(self):
|
|
121
|
+
self.handler.observer = Mock()
|
|
122
|
+
path = os.path.join(config.MODULES_SRC, "cleepbus.tangb", ".git")
|
|
123
|
+
self.handler.on_any_event(DirCreatedEvent(path))
|
|
124
|
+
self.handler.observer.schedule.assert_not_called()
|
|
125
|
+
self.executor.note_change.assert_not_called()
|
|
126
|
+
|
|
127
|
+
def test_schedule_all_is_idempotent(self):
|
|
128
|
+
repo = tempfile.mkdtemp()
|
|
129
|
+
try:
|
|
130
|
+
core = os.path.join(repo, "cleep")
|
|
131
|
+
html = os.path.join(repo, "html")
|
|
132
|
+
bin_dir = os.path.join(repo, "bin")
|
|
133
|
+
modules = os.path.join(repo, "modules")
|
|
134
|
+
os.makedirs(core)
|
|
135
|
+
os.makedirs(html)
|
|
136
|
+
os.makedirs(bin_dir)
|
|
137
|
+
os.makedirs(os.path.join(modules, "audio", "backend"))
|
|
138
|
+
self.handler.observer = Mock()
|
|
139
|
+
with patch.object(config, "CORE_SRC", core), patch.object(
|
|
140
|
+
config, "HTML_SRC", html
|
|
141
|
+
), patch.object(config, "BIN_SRC", bin_dir), patch.object(
|
|
142
|
+
config, "MODULES_SRC", modules
|
|
143
|
+
):
|
|
144
|
+
self.handler.schedule_all()
|
|
145
|
+
first = self.handler.observer.schedule.call_count
|
|
146
|
+
self.handler.schedule_all()
|
|
147
|
+
self.assertEqual(self.handler.observer.schedule.call_count, first)
|
|
148
|
+
self.assertLess(first, 20)
|
|
149
|
+
finally:
|
|
150
|
+
shutil.rmtree(repo)
|
|
151
|
+
|
|
152
|
+
def test_created_module_backend_is_scheduled(self):
|
|
153
|
+
self.handler.observer = Mock()
|
|
154
|
+
path = os.path.join(config.MODULES_SRC, "audio", "backend")
|
|
155
|
+
with patch("os.path.isdir", return_value=True):
|
|
156
|
+
self.handler.on_any_event(DirCreatedEvent(path))
|
|
157
|
+
self.handler.observer.schedule.assert_called_once()
|
|
158
|
+
args, kwargs = self.handler.observer.schedule.call_args
|
|
159
|
+
self.assertEqual(os.path.abspath(args[1]), os.path.abspath(path))
|
|
160
|
+
self.assertTrue(kwargs.get("recursive", args[2] if len(args) > 2 else False))
|
|
161
|
+
self.executor.note_change.assert_called_once_with(
|
|
162
|
+
module="audio", frontend=False, backend=True
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def test_created_core_directory_is_queued(self):
|
|
166
|
+
self.handler.observer = Mock()
|
|
167
|
+
path = os.path.join(config.CORE_SRC, "libs", "newpkg")
|
|
168
|
+
self.handler.on_any_event(DirCreatedEvent(path))
|
|
169
|
+
self.handler.observer.schedule.assert_not_called()
|
|
170
|
+
self.executor.note_change.assert_called_once_with(
|
|
171
|
+
core=True, frontend=False, backend=True
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class QuietPeriodTests(unittest.TestCase):
|
|
176
|
+
def _executor(self):
|
|
177
|
+
executor = ActionsExecutor("https://127.0.0.1:443")
|
|
178
|
+
executor.file = Mock()
|
|
179
|
+
executor.file.core_sync.return_value = True
|
|
180
|
+
executor.file.module_sync.return_value = True
|
|
181
|
+
executor.cleep = Mock()
|
|
182
|
+
executor.quiet_period = 0.2
|
|
183
|
+
executor.poll_interval = 0.05
|
|
184
|
+
return executor
|
|
185
|
+
|
|
186
|
+
def _wait_until(self, predicate, timeout=2.0):
|
|
187
|
+
deadline = time.time() + timeout
|
|
188
|
+
while time.time() < deadline:
|
|
189
|
+
if predicate():
|
|
190
|
+
return True
|
|
191
|
+
time.sleep(0.05)
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
def test_burst_runs_one_sync_and_one_restart(self):
|
|
195
|
+
executor = self._executor()
|
|
196
|
+
executor.start()
|
|
197
|
+
try:
|
|
198
|
+
for _ in range(5):
|
|
199
|
+
executor.note_change(core=True, backend=True)
|
|
200
|
+
time.sleep(0.02)
|
|
201
|
+
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
202
|
+
time.sleep(0.4)
|
|
203
|
+
self.assertEqual(executor.file.core_sync.call_count, 1)
|
|
204
|
+
executor.cleep.restart_backend.assert_called_once()
|
|
205
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
206
|
+
finally:
|
|
207
|
+
executor.stop()
|
|
208
|
+
executor.join(timeout=2)
|
|
209
|
+
|
|
210
|
+
def test_failed_sync_does_not_restart(self):
|
|
211
|
+
executor = self._executor()
|
|
212
|
+
executor.file.core_sync.return_value = False
|
|
213
|
+
executor.start()
|
|
214
|
+
try:
|
|
215
|
+
executor.note_change(core=True, backend=True)
|
|
216
|
+
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
217
|
+
time.sleep(0.4)
|
|
218
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
219
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
220
|
+
finally:
|
|
221
|
+
executor.stop()
|
|
222
|
+
executor.join(timeout=2)
|
|
223
|
+
|
|
224
|
+
def test_frontend_only_refreshes_the_ui(self):
|
|
225
|
+
executor = self._executor()
|
|
226
|
+
executor.start()
|
|
227
|
+
try:
|
|
228
|
+
executor.note_change(module="audio", frontend=True)
|
|
229
|
+
self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
|
|
230
|
+
time.sleep(0.4)
|
|
231
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
232
|
+
executor.cleep.restart_frontend.assert_called_once()
|
|
233
|
+
finally:
|
|
234
|
+
executor.stop()
|
|
235
|
+
executor.join(timeout=2)
|
|
236
|
+
|
|
237
|
+
def test_scripts_only_does_not_restart(self):
|
|
238
|
+
executor = self._executor()
|
|
239
|
+
executor.start()
|
|
240
|
+
try:
|
|
241
|
+
executor.note_change(module="audio")
|
|
242
|
+
self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
|
|
243
|
+
time.sleep(0.4)
|
|
244
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
245
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
246
|
+
finally:
|
|
247
|
+
executor.stop()
|
|
248
|
+
executor.join(timeout=2)
|
|
249
|
+
|
|
250
|
+
def test_core_and_modules_restart_once(self):
|
|
251
|
+
executor = self._executor()
|
|
252
|
+
executor.start()
|
|
253
|
+
try:
|
|
254
|
+
executor.note_change(core=True, backend=True)
|
|
255
|
+
executor.note_change(module="network", backend=True)
|
|
256
|
+
executor.note_change(module="audio", frontend=True)
|
|
257
|
+
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
258
|
+
time.sleep(0.4)
|
|
259
|
+
self.assertEqual(executor.file.core_sync.call_count, 1)
|
|
260
|
+
self.assertEqual(executor.file.module_sync.call_count, 2)
|
|
261
|
+
executor.cleep.restart_backend.assert_called_once()
|
|
262
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
263
|
+
finally:
|
|
264
|
+
executor.stop()
|
|
265
|
+
executor.join(timeout=2)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class InotifyTests(unittest.TestCase):
|
|
269
|
+
def test_module_git_objects_are_not_watched(self):
|
|
270
|
+
repo = tempfile.mkdtemp()
|
|
271
|
+
observer = Observer()
|
|
272
|
+
try:
|
|
273
|
+
modules = os.path.join(repo, "modules")
|
|
274
|
+
git_dir = os.path.join(modules, "audio", ".git", "objects", "ab")
|
|
275
|
+
backend = os.path.join(modules, "audio", "backend")
|
|
276
|
+
os.makedirs(git_dir)
|
|
277
|
+
os.makedirs(backend)
|
|
278
|
+
os.makedirs(os.path.join(repo, "cleep"))
|
|
279
|
+
os.makedirs(os.path.join(repo, "html"))
|
|
280
|
+
os.makedirs(os.path.join(repo, "bin"))
|
|
281
|
+
|
|
282
|
+
executor = Mock()
|
|
283
|
+
handler = CleepHandler(executor)
|
|
284
|
+
handler.observer = observer
|
|
285
|
+
seen = []
|
|
286
|
+
|
|
287
|
+
def record(event):
|
|
288
|
+
seen.append(getattr(event, "src_path", "") or "")
|
|
289
|
+
|
|
290
|
+
handler.on_any_event = record
|
|
291
|
+
with patch.object(config, "CORE_SRC", os.path.join(repo, "cleep")), patch.object(
|
|
292
|
+
config, "HTML_SRC", os.path.join(repo, "html")
|
|
293
|
+
), patch.object(config, "BIN_SRC", os.path.join(repo, "bin")), patch.object(
|
|
294
|
+
config, "MODULES_SRC", modules
|
|
295
|
+
):
|
|
296
|
+
handler.schedule_all()
|
|
297
|
+
observer.start()
|
|
298
|
+
time.sleep(0.2)
|
|
299
|
+
with open(os.path.join(git_dir, "deadbeef"), "w", encoding="utf-8") as handle:
|
|
300
|
+
handle.write("x")
|
|
301
|
+
with open(os.path.join(backend, "audio.py"), "w", encoding="utf-8") as handle:
|
|
302
|
+
handle.write("x")
|
|
303
|
+
time.sleep(0.5)
|
|
304
|
+
finally:
|
|
305
|
+
observer.stop()
|
|
306
|
+
observer.join(timeout=2)
|
|
307
|
+
shutil.rmtree(repo)
|
|
308
|
+
|
|
309
|
+
self.assertTrue(any(path.endswith("audio.py") for path in seen))
|
|
310
|
+
self.assertFalse(any(".git" in path.split(os.sep) for path in seen))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
unittest.main()
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import shutil
|
|
6
|
-
import tempfile
|
|
7
|
-
import time
|
|
8
|
-
import unittest
|
|
9
|
-
from unittest.mock import Mock
|
|
10
|
-
|
|
11
|
-
from watchdog.events import DirCreatedEvent, FileModifiedEvent, FileMovedEvent
|
|
12
|
-
from watchdog.observers import Observer
|
|
13
|
-
|
|
14
|
-
from cleepcli import config
|
|
15
|
-
from cleepcli.watch import ActionsExecutor, CleepHandler, iter_watch_directories
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class IterWatchDirectoriesTests(unittest.TestCase):
|
|
19
|
-
def test_skips_rejected_directories(self):
|
|
20
|
-
root = tempfile.mkdtemp()
|
|
21
|
-
try:
|
|
22
|
-
os.makedirs(os.path.join(root, ".git", "objects", "ab"))
|
|
23
|
-
os.makedirs(os.path.join(root, "pkg", "__pycache__"))
|
|
24
|
-
os.makedirs(os.path.join(root, "pkg", "tests"))
|
|
25
|
-
os.makedirs(os.path.join(root, "pkg", "node_modules", "lib"))
|
|
26
|
-
os.makedirs(os.path.join(root, "pkg", "backend"))
|
|
27
|
-
watched = set(iter_watch_directories(root))
|
|
28
|
-
finally:
|
|
29
|
-
shutil.rmtree(root)
|
|
30
|
-
|
|
31
|
-
self.assertIn(root, watched)
|
|
32
|
-
self.assertIn(os.path.join(root, "pkg"), watched)
|
|
33
|
-
self.assertIn(os.path.join(root, "pkg", "backend"), watched)
|
|
34
|
-
self.assertFalse(any(".git" in path.split(os.sep) for path in watched))
|
|
35
|
-
self.assertFalse(any("__pycache__" in path.split(os.sep) for path in watched))
|
|
36
|
-
self.assertFalse(any("tests" in path.split(os.sep) for path in watched))
|
|
37
|
-
self.assertFalse(any("node_modules" in path.split(os.sep) for path in watched))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class HandlerTests(unittest.TestCase):
|
|
41
|
-
def setUp(self):
|
|
42
|
-
self.executor = Mock()
|
|
43
|
-
self.handler = CleepHandler(self.executor)
|
|
44
|
-
|
|
45
|
-
def test_git_file_is_ignored(self):
|
|
46
|
-
path = os.path.join(
|
|
47
|
-
config.MODULES_SRC, "cleepbus.tangb", ".git", "objects", "ab", "deadbeef"
|
|
48
|
-
)
|
|
49
|
-
self.handler.on_any_event(FileModifiedEvent(path))
|
|
50
|
-
self.executor.note_change.assert_not_called()
|
|
51
|
-
|
|
52
|
-
def test_tests_file_is_ignored(self):
|
|
53
|
-
path = os.path.join(config.CORE_SRC, "libs", "tests", "test_rpcserver.py")
|
|
54
|
-
self.handler.on_any_event(FileModifiedEvent(path))
|
|
55
|
-
self.executor.note_change.assert_not_called()
|
|
56
|
-
|
|
57
|
-
def test_core_file_is_queued(self):
|
|
58
|
-
path = os.path.join(config.CORE_SRC, "rpcserver.py")
|
|
59
|
-
self.handler.on_any_event(FileModifiedEvent(path))
|
|
60
|
-
self.executor.note_change.assert_called_once_with(core=True, frontend=False)
|
|
61
|
-
|
|
62
|
-
def test_atomic_save_move_is_queued(self):
|
|
63
|
-
src = os.path.join(config.CORE_SRC, ".rpcserver.py.tmp")
|
|
64
|
-
dest = os.path.join(config.CORE_SRC, "rpcserver.py")
|
|
65
|
-
self.handler.on_any_event(FileMovedEvent(src, dest))
|
|
66
|
-
self.executor.note_change.assert_called_once_with(core=True, frontend=False)
|
|
67
|
-
|
|
68
|
-
def test_created_git_directory_is_not_watched(self):
|
|
69
|
-
self.handler.observer = Mock()
|
|
70
|
-
path = os.path.join(config.MODULES_SRC, "cleepbus.tangb", ".git")
|
|
71
|
-
self.handler.on_any_event(DirCreatedEvent(path))
|
|
72
|
-
self.handler.observer.schedule.assert_not_called()
|
|
73
|
-
self.executor.note_change.assert_not_called()
|
|
74
|
-
|
|
75
|
-
def test_directory_is_scheduled_once(self):
|
|
76
|
-
root = tempfile.mkdtemp()
|
|
77
|
-
try:
|
|
78
|
-
os.makedirs(os.path.join(root, "pkg"))
|
|
79
|
-
self.handler.observer = Mock()
|
|
80
|
-
self.handler.schedule_tree(root)
|
|
81
|
-
self.handler.schedule_tree(root)
|
|
82
|
-
self.assertEqual(self.handler.observer.schedule.call_count, 2)
|
|
83
|
-
finally:
|
|
84
|
-
shutil.rmtree(root)
|
|
85
|
-
|
|
86
|
-
def test_created_core_directory_is_queued(self):
|
|
87
|
-
self.handler.observer = Mock()
|
|
88
|
-
path = os.path.join(config.CORE_SRC, "libs", "newpkg")
|
|
89
|
-
self.handler.on_any_event(DirCreatedEvent(path))
|
|
90
|
-
self.executor.note_change.assert_called_once_with(core=True, frontend=False)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class QuietPeriodTests(unittest.TestCase):
|
|
94
|
-
def _executor(self):
|
|
95
|
-
executor = ActionsExecutor("https://127.0.0.1:443")
|
|
96
|
-
executor.file = Mock()
|
|
97
|
-
executor.file.core_sync.return_value = True
|
|
98
|
-
executor.file.module_sync.return_value = True
|
|
99
|
-
executor.cleep = Mock()
|
|
100
|
-
executor.quiet_period = 0.2
|
|
101
|
-
executor.poll_interval = 0.05
|
|
102
|
-
return executor
|
|
103
|
-
|
|
104
|
-
def _wait_until(self, predicate, timeout=2.0):
|
|
105
|
-
deadline = time.time() + timeout
|
|
106
|
-
while time.time() < deadline:
|
|
107
|
-
if predicate():
|
|
108
|
-
return True
|
|
109
|
-
time.sleep(0.05)
|
|
110
|
-
return False
|
|
111
|
-
|
|
112
|
-
def test_burst_runs_one_sync_and_one_restart(self):
|
|
113
|
-
executor = self._executor()
|
|
114
|
-
executor.start()
|
|
115
|
-
try:
|
|
116
|
-
for _ in range(5):
|
|
117
|
-
executor.note_change(core=True, frontend=False)
|
|
118
|
-
time.sleep(0.02)
|
|
119
|
-
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
120
|
-
time.sleep(0.4)
|
|
121
|
-
self.assertEqual(executor.file.core_sync.call_count, 1)
|
|
122
|
-
executor.cleep.restart_backend.assert_called_once()
|
|
123
|
-
executor.cleep.restart_frontend.assert_not_called()
|
|
124
|
-
finally:
|
|
125
|
-
executor.stop()
|
|
126
|
-
executor.join(timeout=2)
|
|
127
|
-
|
|
128
|
-
def test_failed_sync_does_not_restart(self):
|
|
129
|
-
executor = self._executor()
|
|
130
|
-
executor.file.core_sync.return_value = False
|
|
131
|
-
executor.start()
|
|
132
|
-
try:
|
|
133
|
-
executor.note_change(core=True, frontend=False)
|
|
134
|
-
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
135
|
-
time.sleep(0.4)
|
|
136
|
-
executor.cleep.restart_backend.assert_not_called()
|
|
137
|
-
executor.cleep.restart_frontend.assert_not_called()
|
|
138
|
-
finally:
|
|
139
|
-
executor.stop()
|
|
140
|
-
executor.join(timeout=2)
|
|
141
|
-
|
|
142
|
-
def test_frontend_only_refreshes_the_ui(self):
|
|
143
|
-
executor = self._executor()
|
|
144
|
-
executor.start()
|
|
145
|
-
try:
|
|
146
|
-
executor.note_change(module="audio", frontend=True)
|
|
147
|
-
self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
|
|
148
|
-
time.sleep(0.4)
|
|
149
|
-
executor.cleep.restart_backend.assert_not_called()
|
|
150
|
-
executor.cleep.restart_frontend.assert_called_once()
|
|
151
|
-
finally:
|
|
152
|
-
executor.stop()
|
|
153
|
-
executor.join(timeout=2)
|
|
154
|
-
|
|
155
|
-
def test_core_and_modules_restart_once(self):
|
|
156
|
-
executor = self._executor()
|
|
157
|
-
executor.start()
|
|
158
|
-
try:
|
|
159
|
-
executor.note_change(core=True, frontend=False)
|
|
160
|
-
executor.note_change(module="network", frontend=False)
|
|
161
|
-
executor.note_change(module="audio", frontend=True)
|
|
162
|
-
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
163
|
-
time.sleep(0.4)
|
|
164
|
-
self.assertEqual(executor.file.core_sync.call_count, 1)
|
|
165
|
-
self.assertEqual(executor.file.module_sync.call_count, 2)
|
|
166
|
-
executor.cleep.restart_backend.assert_called_once()
|
|
167
|
-
executor.cleep.restart_frontend.assert_not_called()
|
|
168
|
-
finally:
|
|
169
|
-
executor.stop()
|
|
170
|
-
executor.join(timeout=2)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class InotifyTests(unittest.TestCase):
|
|
174
|
-
def test_git_objects_are_not_watched(self):
|
|
175
|
-
root = tempfile.mkdtemp()
|
|
176
|
-
observer = Observer()
|
|
177
|
-
try:
|
|
178
|
-
git_dir = os.path.join(root, ".git", "objects", "ab")
|
|
179
|
-
os.makedirs(git_dir)
|
|
180
|
-
os.makedirs(os.path.join(root, "pkg"))
|
|
181
|
-
executor = Mock()
|
|
182
|
-
handler = CleepHandler(executor)
|
|
183
|
-
seen = []
|
|
184
|
-
|
|
185
|
-
def record(event):
|
|
186
|
-
seen.append(event.src_path)
|
|
187
|
-
|
|
188
|
-
handler.on_any_event = record
|
|
189
|
-
for directory in iter_watch_directories(root):
|
|
190
|
-
observer.schedule(handler, directory, recursive=False)
|
|
191
|
-
observer.start()
|
|
192
|
-
time.sleep(0.2)
|
|
193
|
-
with open(os.path.join(git_dir, "deadbeef"), "w", encoding="utf-8") as handle:
|
|
194
|
-
handle.write("x")
|
|
195
|
-
with open(os.path.join(root, "pkg", "a.py"), "w", encoding="utf-8") as handle:
|
|
196
|
-
handle.write("x")
|
|
197
|
-
time.sleep(0.5)
|
|
198
|
-
finally:
|
|
199
|
-
observer.stop()
|
|
200
|
-
observer.join(timeout=2)
|
|
201
|
-
shutil.rmtree(root)
|
|
202
|
-
|
|
203
|
-
self.assertTrue(any(path.endswith("a.py") for path in seen))
|
|
204
|
-
self.assertFalse(any(".git" in path.split(os.sep) for path in seen))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if __name__ == "__main__":
|
|
208
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|