cleepcli 1.44.0__tar.gz → 1.44.3__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.3}/PKG-INFO +1 -1
- {cleepcli-1.44.0 → cleepcli-1.44.3}/bin/ccli +15 -4
- {cleepcli-1.44.0 → cleepcli-1.44.3}/bin/cleep-cli +15 -4
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/cleepapi.py +33 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/file.py +61 -34
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/version.py +1 -1
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/watch.py +175 -66
- {cleepcli-1.44.0 → cleepcli-1.44.3/cleepcli.egg-info}/PKG-INFO +1 -1
- cleepcli-1.44.3/tests/test_watch.py +319 -0
- cleepcli-1.44.0/tests/test_watch.py +0 -208
- {cleepcli-1.44.0 → cleepcli-1.44.3}/LICENSE +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/MANIFEST.in +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/README.md +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/__init__.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/check.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/ci.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/config.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/console.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/docs.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/git.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/module.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/package.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/test.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/tools.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/SOURCES.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/dependency_links.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/requires.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/top_level.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/pyproject.toml +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/requirements.txt +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/setup.cfg +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/setup.py +0 -0
- {cleepcli-1.44.0 → cleepcli-1.44.3}/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
|
|
|
@@ -243,6 +243,39 @@ class CleepApi():
|
|
|
243
243
|
return False
|
|
244
244
|
return True
|
|
245
245
|
|
|
246
|
+
def notify_sync_started(self):
|
|
247
|
+
"""
|
|
248
|
+
Tell the developer UI a watch sync is starting.
|
|
249
|
+
"""
|
|
250
|
+
data = {'to': 'developer', 'command': 'notify_sync_started'}
|
|
251
|
+
result = self.__post(self.command_url, data)
|
|
252
|
+
if not result or result[0] != 200:
|
|
253
|
+
self.logger.debug('Unable to notify sync started')
|
|
254
|
+
return False
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def notify_sync_done(self):
|
|
258
|
+
"""
|
|
259
|
+
Tell the developer UI a watch sync finished without restart.
|
|
260
|
+
"""
|
|
261
|
+
data = {'to': 'developer', 'command': 'notify_sync_done'}
|
|
262
|
+
result = self.__post(self.command_url, data)
|
|
263
|
+
if not result or result[0] != 200:
|
|
264
|
+
self.logger.debug('Unable to notify sync done')
|
|
265
|
+
return False
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def notify_sync_failed(self):
|
|
269
|
+
"""
|
|
270
|
+
Tell the developer UI a watch sync failed.
|
|
271
|
+
"""
|
|
272
|
+
data = {'to': 'developer', 'command': 'notify_sync_failed'}
|
|
273
|
+
result = self.__post(self.command_url, data)
|
|
274
|
+
if not result or result[0] != 200:
|
|
275
|
+
self.logger.debug('Unable to notify sync failed')
|
|
276
|
+
return False
|
|
277
|
+
return True
|
|
278
|
+
|
|
246
279
|
def get_documentation(self, module_name):
|
|
247
280
|
"""
|
|
248
281
|
Call endpoint to get documentation for specified application
|
|
@@ -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):
|
|
38
53
|
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
|
|
55
|
+
yield config.MODULES_SRC, False
|
|
56
|
+
try:
|
|
57
|
+
names = sorted(os.listdir(config.MODULES_SRC))
|
|
58
|
+
except OSError:
|
|
59
|
+
return
|
|
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,40 +124,60 @@ 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).
|
|
133
|
+
|
|
134
|
+
The first change of a batch notifies the UI immediately so the wait
|
|
135
|
+
for the quiet period is visible.
|
|
102
136
|
"""
|
|
137
|
+
notify_started = False
|
|
103
138
|
with self._lock:
|
|
139
|
+
notify_started = not self._batch.pending()
|
|
104
140
|
if module:
|
|
105
141
|
self._batch.modules.add(module)
|
|
106
142
|
if core:
|
|
107
143
|
self._batch.core = True
|
|
108
144
|
if frontend:
|
|
109
145
|
self._batch.frontend = True
|
|
110
|
-
|
|
146
|
+
if backend:
|
|
111
147
|
self._batch.backend = True
|
|
112
148
|
self._batch.last = time.monotonic()
|
|
149
|
+
if notify_started:
|
|
150
|
+
self.cleep.notify_sync_started()
|
|
113
151
|
|
|
114
152
|
def _run_batch(self, batch):
|
|
115
153
|
failed = False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
try:
|
|
155
|
+
if batch.core:
|
|
156
|
+
self.logger.info("Syncing core")
|
|
157
|
+
if not self.file.core_sync():
|
|
158
|
+
failed = True
|
|
159
|
+
for module in sorted(batch.modules):
|
|
160
|
+
self.logger.info('Syncing module "%s"', module)
|
|
161
|
+
if not self.file.module_sync(module):
|
|
162
|
+
failed = True
|
|
163
|
+
if failed:
|
|
164
|
+
self.logger.error("Sync failed, Cleep was not restarted")
|
|
165
|
+
self.cleep.notify_sync_failed()
|
|
166
|
+
return
|
|
167
|
+
if batch.backend:
|
|
168
|
+
# Backend restart drops the UI connection. The page reloads on its own
|
|
169
|
+
# once Cleep answers again, with the frontend files already synced.
|
|
170
|
+
if not self.cleep.restart_backend():
|
|
171
|
+
self.cleep.notify_sync_failed()
|
|
172
|
+
elif batch.frontend:
|
|
173
|
+
if not self.cleep.restart_frontend():
|
|
174
|
+
self.cleep.notify_sync_failed()
|
|
175
|
+
else:
|
|
176
|
+
# Scripts-only: files synced, Cleep keeps running
|
|
177
|
+
self.cleep.notify_sync_done()
|
|
178
|
+
except Exception:
|
|
179
|
+
self.cleep.notify_sync_failed()
|
|
180
|
+
raise
|
|
133
181
|
|
|
134
182
|
def run(self):
|
|
135
183
|
"""
|
|
@@ -200,24 +248,54 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
200
248
|
return getattr(event, "dest_path", None) or event.src_path
|
|
201
249
|
return None
|
|
202
250
|
|
|
203
|
-
def
|
|
251
|
+
def schedule_all(self):
|
|
204
252
|
"""
|
|
205
|
-
|
|
253
|
+
Install the few recursive watches needed for core and module sources.
|
|
206
254
|
"""
|
|
207
|
-
for
|
|
208
|
-
self.
|
|
255
|
+
for path, recursive in iter_watch_roots():
|
|
256
|
+
self._schedule(path, recursive=recursive)
|
|
209
257
|
|
|
210
|
-
def
|
|
258
|
+
def _schedule(self, path, recursive=True):
|
|
211
259
|
"""
|
|
212
|
-
Watch
|
|
260
|
+
Watch path once. Rejected names and duplicates are skipped.
|
|
213
261
|
"""
|
|
214
262
|
if self.observer is None or not path or self._rejected_path(path):
|
|
215
|
-
return
|
|
263
|
+
return False
|
|
216
264
|
real = os.path.abspath(path)
|
|
217
265
|
if real in self._watched or not os.path.isdir(real):
|
|
218
|
-
return
|
|
219
|
-
self.observer.schedule(self, real, recursive=
|
|
266
|
+
return False
|
|
267
|
+
self.observer.schedule(self, real, recursive=recursive)
|
|
220
268
|
self._watched.add(real)
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
def _schedule_module(self, module_path):
|
|
272
|
+
"""
|
|
273
|
+
Watch a module root (non-recursive) and its content dirs. Never watches .git.
|
|
274
|
+
"""
|
|
275
|
+
if not module_path or not os.path.isdir(module_path):
|
|
276
|
+
return
|
|
277
|
+
if os.path.basename(module_path) in self.REJECTED_DIRS:
|
|
278
|
+
return
|
|
279
|
+
self._schedule(module_path, recursive=False)
|
|
280
|
+
for sub in MODULE_CONTENT_DIRS:
|
|
281
|
+
self._schedule(os.path.join(module_path, sub), recursive=True)
|
|
282
|
+
|
|
283
|
+
def _maybe_schedule_new_path(self, path):
|
|
284
|
+
"""
|
|
285
|
+
Add watches when a new module or module content dir appears.
|
|
286
|
+
|
|
287
|
+
Core/html/bin are already recursive, so new dirs there need no schedule.
|
|
288
|
+
"""
|
|
289
|
+
if not path or self._rejected_path(path):
|
|
290
|
+
return
|
|
291
|
+
parent = os.path.dirname(path)
|
|
292
|
+
name = os.path.basename(path)
|
|
293
|
+
modules_root = os.path.abspath(config.MODULES_SRC)
|
|
294
|
+
if os.path.abspath(parent) == modules_root:
|
|
295
|
+
self._schedule_module(path)
|
|
296
|
+
return
|
|
297
|
+
if name in MODULE_CONTENT_DIRS and os.path.abspath(os.path.dirname(parent)) == modules_root:
|
|
298
|
+
self._schedule(path, recursive=True)
|
|
221
299
|
|
|
222
300
|
def _module_name_from_path(self, path):
|
|
223
301
|
prefix = config.MODULES_SRC.rstrip(os.sep) + os.sep
|
|
@@ -313,7 +391,7 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
313
391
|
Returns:
|
|
314
392
|
module name (string) or None if nothing found
|
|
315
393
|
"""
|
|
316
|
-
pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)
|
|
394
|
+
pattern = r"^%s/(.*?)/(?:frontend|backend|scripts)(?:/.*)?$" % (
|
|
317
395
|
re.escape(config.MODULES_SRC)
|
|
318
396
|
)
|
|
319
397
|
# self.logger.debug('Module pattern: %s' % pattern)
|
|
@@ -364,7 +442,7 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
364
442
|
Returns:
|
|
365
443
|
True if change on frontend, False otherwise
|
|
366
444
|
"""
|
|
367
|
-
pattern = r"^(%s/|%s/(?:.*?)/frontend
|
|
445
|
+
pattern = r"^(%s/|%s/(?:.*?)/frontend(?:/)?).*$" % (
|
|
368
446
|
re.escape(config.HTML_SRC),
|
|
369
447
|
re.escape(config.MODULES_SRC),
|
|
370
448
|
)
|
|
@@ -376,16 +454,31 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
376
454
|
|
|
377
455
|
return False
|
|
378
456
|
|
|
457
|
+
def __change_on_scripts(self, event):
|
|
458
|
+
"""
|
|
459
|
+
Detect change on module install scripts
|
|
460
|
+
"""
|
|
461
|
+
pattern = r"^%s/(?:.*?)/scripts(?:/.*)?$" % (re.escape(config.MODULES_SRC))
|
|
462
|
+
path = self._event_path(event)
|
|
463
|
+
return bool(path and re.search(pattern, path))
|
|
464
|
+
|
|
379
465
|
def _queue_change(self, event):
|
|
380
466
|
frontend = self.__change_on_frontend(event)
|
|
467
|
+
scripts = self.__change_on_scripts(event)
|
|
381
468
|
module = self.__change_on_module(event)
|
|
382
469
|
core = module is None and self.__change_on_core(event)
|
|
470
|
+
# Scripts are synced for the next install; they do not reload Cleep.
|
|
471
|
+
backend = not frontend and not scripts
|
|
383
472
|
if module:
|
|
384
473
|
self.logger.debug(' Change on module "%s"' % module)
|
|
385
|
-
self.actions_executor.note_change(
|
|
474
|
+
self.actions_executor.note_change(
|
|
475
|
+
module=module, frontend=frontend, backend=backend
|
|
476
|
+
)
|
|
386
477
|
elif core:
|
|
387
478
|
self.logger.debug(" Change on core")
|
|
388
|
-
self.actions_executor.note_change(
|
|
479
|
+
self.actions_executor.note_change(
|
|
480
|
+
core=True, frontend=frontend, backend=backend
|
|
481
|
+
)
|
|
389
482
|
else:
|
|
390
483
|
self.logger.debug(" Dropped due to useless change")
|
|
391
484
|
|
|
@@ -394,22 +487,25 @@ class CleepHandler(FileSystemEventHandler):
|
|
|
394
487
|
self.logger.debug("Event: %s" % event)
|
|
395
488
|
created = self._created_directory(event)
|
|
396
489
|
if created:
|
|
397
|
-
self.schedule_tree(created)
|
|
398
490
|
if self._rejected_path(created):
|
|
399
491
|
self.logger.debug(" Filtered: directory")
|
|
400
492
|
return
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
self.actions_executor.note_change(module=module, frontend=frontend)
|
|
493
|
+
self._maybe_schedule_new_path(created)
|
|
494
|
+
# New module root: only schedule watches; wait for content files
|
|
495
|
+
modules_root = os.path.abspath(config.MODULES_SRC)
|
|
496
|
+
if os.path.abspath(os.path.dirname(created)) == modules_root:
|
|
406
497
|
return
|
|
407
|
-
if
|
|
498
|
+
if self.__change_on_module(event):
|
|
499
|
+
self._queue_change(event)
|
|
500
|
+
elif _is_under(created, config.CORE_SRC) or _is_under(
|
|
408
501
|
created, config.HTML_SRC
|
|
409
502
|
) or _is_under(created, config.BIN_SRC):
|
|
410
503
|
frontend = self.__change_on_frontend(event)
|
|
411
|
-
self.
|
|
412
|
-
|
|
504
|
+
self.actions_executor.note_change(
|
|
505
|
+
core=True,
|
|
506
|
+
frontend=frontend,
|
|
507
|
+
backend=not frontend,
|
|
508
|
+
)
|
|
413
509
|
return
|
|
414
510
|
|
|
415
511
|
if self.__is_event_dropped(event):
|
|
@@ -451,25 +547,38 @@ class CleepWatchdog:
|
|
|
451
547
|
event_handler = CleepHandler(self.actions_executor)
|
|
452
548
|
observer = Observer()
|
|
453
549
|
event_handler.observer = observer
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if not
|
|
550
|
+
try:
|
|
551
|
+
event_handler.schedule_all()
|
|
552
|
+
except OSError as error:
|
|
553
|
+
self.logger.error(
|
|
554
|
+
"Unable to start filesystem watches (%s). "
|
|
555
|
+
"Check fs.inotify.max_user_instances / max_user_watches",
|
|
556
|
+
error,
|
|
557
|
+
)
|
|
558
|
+
self.actions_executor.stop()
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
if not event_handler._watched:
|
|
466
562
|
self.logger.error(
|
|
467
563
|
'Please get Cleep sources using "cleep-cli getcore" command'
|
|
468
564
|
)
|
|
565
|
+
self.actions_executor.stop()
|
|
469
566
|
return False
|
|
470
567
|
|
|
471
|
-
self.logger.info(
|
|
472
|
-
|
|
568
|
+
self.logger.info(
|
|
569
|
+
'Cleep watchdog is running on "%s" (%d watches, CTRL-C to stop)'
|
|
570
|
+
% (path, len(event_handler._watched))
|
|
571
|
+
)
|
|
572
|
+
try:
|
|
573
|
+
observer.start()
|
|
574
|
+
except OSError as error:
|
|
575
|
+
self.logger.error(
|
|
576
|
+
"Unable to start filesystem watches (%s). "
|
|
577
|
+
"Check fs.inotify.max_user_instances / max_user_watches",
|
|
578
|
+
error,
|
|
579
|
+
)
|
|
580
|
+
self.actions_executor.stop()
|
|
581
|
+
return False
|
|
473
582
|
try:
|
|
474
583
|
while True:
|
|
475
584
|
time.sleep(0.25)
|
|
@@ -0,0 +1,319 @@
|
|
|
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.notify_sync_started.assert_called_once()
|
|
205
|
+
executor.cleep.restart_backend.assert_called_once()
|
|
206
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
207
|
+
executor.cleep.notify_sync_done.assert_not_called()
|
|
208
|
+
finally:
|
|
209
|
+
executor.stop()
|
|
210
|
+
executor.join(timeout=2)
|
|
211
|
+
|
|
212
|
+
def test_failed_sync_does_not_restart(self):
|
|
213
|
+
executor = self._executor()
|
|
214
|
+
executor.file.core_sync.return_value = False
|
|
215
|
+
executor.start()
|
|
216
|
+
try:
|
|
217
|
+
executor.note_change(core=True, backend=True)
|
|
218
|
+
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
219
|
+
time.sleep(0.4)
|
|
220
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
221
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
222
|
+
executor.cleep.notify_sync_failed.assert_called_once()
|
|
223
|
+
finally:
|
|
224
|
+
executor.stop()
|
|
225
|
+
executor.join(timeout=2)
|
|
226
|
+
|
|
227
|
+
def test_frontend_only_refreshes_the_ui(self):
|
|
228
|
+
executor = self._executor()
|
|
229
|
+
executor.start()
|
|
230
|
+
try:
|
|
231
|
+
executor.note_change(module="audio", frontend=True)
|
|
232
|
+
self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
|
|
233
|
+
time.sleep(0.4)
|
|
234
|
+
executor.cleep.notify_sync_started.assert_called_once()
|
|
235
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
236
|
+
executor.cleep.restart_frontend.assert_called_once()
|
|
237
|
+
finally:
|
|
238
|
+
executor.stop()
|
|
239
|
+
executor.join(timeout=2)
|
|
240
|
+
|
|
241
|
+
def test_scripts_only_does_not_restart(self):
|
|
242
|
+
executor = self._executor()
|
|
243
|
+
executor.start()
|
|
244
|
+
try:
|
|
245
|
+
executor.note_change(module="audio")
|
|
246
|
+
self.assertTrue(self._wait_until(lambda: executor.file.module_sync.call_count >= 1))
|
|
247
|
+
time.sleep(0.4)
|
|
248
|
+
executor.cleep.restart_backend.assert_not_called()
|
|
249
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
250
|
+
executor.cleep.notify_sync_done.assert_called_once()
|
|
251
|
+
finally:
|
|
252
|
+
executor.stop()
|
|
253
|
+
executor.join(timeout=2)
|
|
254
|
+
|
|
255
|
+
def test_core_and_modules_restart_once(self):
|
|
256
|
+
executor = self._executor()
|
|
257
|
+
executor.start()
|
|
258
|
+
try:
|
|
259
|
+
executor.note_change(core=True, backend=True)
|
|
260
|
+
executor.note_change(module="network", backend=True)
|
|
261
|
+
executor.note_change(module="audio", frontend=True)
|
|
262
|
+
self.assertTrue(self._wait_until(lambda: executor.file.core_sync.call_count >= 1))
|
|
263
|
+
time.sleep(0.4)
|
|
264
|
+
self.assertEqual(executor.file.core_sync.call_count, 1)
|
|
265
|
+
self.assertEqual(executor.file.module_sync.call_count, 2)
|
|
266
|
+
executor.cleep.restart_backend.assert_called_once()
|
|
267
|
+
executor.cleep.restart_frontend.assert_not_called()
|
|
268
|
+
finally:
|
|
269
|
+
executor.stop()
|
|
270
|
+
executor.join(timeout=2)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class InotifyTests(unittest.TestCase):
|
|
274
|
+
def test_module_git_objects_are_not_watched(self):
|
|
275
|
+
repo = tempfile.mkdtemp()
|
|
276
|
+
observer = Observer()
|
|
277
|
+
try:
|
|
278
|
+
modules = os.path.join(repo, "modules")
|
|
279
|
+
git_dir = os.path.join(modules, "audio", ".git", "objects", "ab")
|
|
280
|
+
backend = os.path.join(modules, "audio", "backend")
|
|
281
|
+
os.makedirs(git_dir)
|
|
282
|
+
os.makedirs(backend)
|
|
283
|
+
os.makedirs(os.path.join(repo, "cleep"))
|
|
284
|
+
os.makedirs(os.path.join(repo, "html"))
|
|
285
|
+
os.makedirs(os.path.join(repo, "bin"))
|
|
286
|
+
|
|
287
|
+
executor = Mock()
|
|
288
|
+
handler = CleepHandler(executor)
|
|
289
|
+
handler.observer = observer
|
|
290
|
+
seen = []
|
|
291
|
+
|
|
292
|
+
def record(event):
|
|
293
|
+
seen.append(getattr(event, "src_path", "") or "")
|
|
294
|
+
|
|
295
|
+
handler.on_any_event = record
|
|
296
|
+
with patch.object(config, "CORE_SRC", os.path.join(repo, "cleep")), patch.object(
|
|
297
|
+
config, "HTML_SRC", os.path.join(repo, "html")
|
|
298
|
+
), patch.object(config, "BIN_SRC", os.path.join(repo, "bin")), patch.object(
|
|
299
|
+
config, "MODULES_SRC", modules
|
|
300
|
+
):
|
|
301
|
+
handler.schedule_all()
|
|
302
|
+
observer.start()
|
|
303
|
+
time.sleep(0.2)
|
|
304
|
+
with open(os.path.join(git_dir, "deadbeef"), "w", encoding="utf-8") as handle:
|
|
305
|
+
handle.write("x")
|
|
306
|
+
with open(os.path.join(backend, "audio.py"), "w", encoding="utf-8") as handle:
|
|
307
|
+
handle.write("x")
|
|
308
|
+
time.sleep(0.5)
|
|
309
|
+
finally:
|
|
310
|
+
observer.stop()
|
|
311
|
+
observer.join(timeout=2)
|
|
312
|
+
shutil.rmtree(repo)
|
|
313
|
+
|
|
314
|
+
self.assertTrue(any(path.endswith("audio.py") for path in seen))
|
|
315
|
+
self.assertFalse(any(".git" in path.split(os.sep) for path in seen))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
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
|