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.
Files changed (33) hide show
  1. {cleepcli-1.44.0/cleepcli.egg-info → cleepcli-1.44.3}/PKG-INFO +1 -1
  2. {cleepcli-1.44.0 → cleepcli-1.44.3}/bin/ccli +15 -4
  3. {cleepcli-1.44.0 → cleepcli-1.44.3}/bin/cleep-cli +15 -4
  4. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/cleepapi.py +33 -0
  5. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/file.py +61 -34
  6. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/version.py +1 -1
  7. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/watch.py +175 -66
  8. {cleepcli-1.44.0 → cleepcli-1.44.3/cleepcli.egg-info}/PKG-INFO +1 -1
  9. cleepcli-1.44.3/tests/test_watch.py +319 -0
  10. cleepcli-1.44.0/tests/test_watch.py +0 -208
  11. {cleepcli-1.44.0 → cleepcli-1.44.3}/LICENSE +0 -0
  12. {cleepcli-1.44.0 → cleepcli-1.44.3}/MANIFEST.in +0 -0
  13. {cleepcli-1.44.0 → cleepcli-1.44.3}/README.md +0 -0
  14. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/__init__.py +0 -0
  15. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/check.py +0 -0
  16. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/ci.py +0 -0
  17. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/config.py +0 -0
  18. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/console.py +0 -0
  19. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/docs.py +0 -0
  20. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/git.py +0 -0
  21. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/module.py +0 -0
  22. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/package.py +0 -0
  23. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/test.py +0 -0
  24. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli/tools.py +0 -0
  25. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/SOURCES.txt +0 -0
  26. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/dependency_links.txt +0 -0
  27. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/requires.txt +0 -0
  28. {cleepcli-1.44.0 → cleepcli-1.44.3}/cleepcli.egg-info/top_level.txt +0 -0
  29. {cleepcli-1.44.0 → cleepcli-1.44.3}/pyproject.toml +0 -0
  30. {cleepcli-1.44.0 → cleepcli-1.44.3}/requirements.txt +0 -0
  31. {cleepcli-1.44.0 → cleepcli-1.44.3}/setup.cfg +0 -0
  32. {cleepcli-1.44.0 → cleepcli-1.44.3}/setup.py +0 -0
  33. {cleepcli-1.44.0 → cleepcli-1.44.3}/tests/test_console.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.44.0
3
+ Version: 1.44.3
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -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
- res = f.module_run_install_scripts(module)
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
- res = f.module_run_install_scripts(module)
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/modules"
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
- c = Console()
75
- cmd = """
76
- set -e
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
- 'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst,
91
- 'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst,
92
- 'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst
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 module_run_install_scripts(self, module_name):
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
- preinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "preinst.sh")
115
- postinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "postinst.sh")
116
- console = Console()
117
-
118
- if os.path.exists(preinst_script):
119
- self.logger.info('Running %s scripts', preinst_script)
120
- resp = console.command(preinst_script, timeout=300)
121
- if resp["killed"] or resp["returncode"] != 0:
122
- self.logger.error('Error running %s [%s]: %s', preinst_script, resp["returncode"], ''.join(resp["stderr"]))
123
- return False
124
-
125
- if os.path.exists(postinst_script):
126
- self.logger.info('Running %s scripts', postinst_script)
127
- resp = console.command(postinst_script, timeout=300)
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
+ )
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- VERSION = "1.44.0"
4
+ VERSION = "1.44.3"
@@ -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 = (EVENT_TYPE_CREATED, EVENT_TYPE_MODIFIED, EVENT_TYPE_MOVED)
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
- def iter_watch_directories(root):
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 directories to watch under root.
43
+ Yield (path, recursive) pairs to schedule on the observer.
33
44
 
34
- Rejected directory names are not yielded and are not descended into, so
35
- inotify never watches them.
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
- if not os.path.isdir(root):
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
- for dirpath, dirnames, _filenames in os.walk(root):
40
- dirnames[:] = [
41
- name for name in dirnames if name not in CleepHandler.REJECTED_DIRS
42
- ]
43
- yield dirpath
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
- else:
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
- if batch.core:
117
- self.logger.info("Syncing core")
118
- if not self.file.core_sync():
119
- failed = True
120
- for module in sorted(batch.modules):
121
- self.logger.info('Syncing module "%s"', module)
122
- if not self.file.module_sync(module):
123
- failed = True
124
- if failed:
125
- self.logger.error("Sync failed, Cleep was not restarted")
126
- return
127
- if batch.backend:
128
- # Backend restart drops the UI connection. The page reloads on its own
129
- # once Cleep answers again, with the frontend files already synced.
130
- self.cleep.restart_backend()
131
- elif batch.frontend:
132
- self.cleep.restart_frontend()
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 schedule_tree(self, root):
251
+ def schedule_all(self):
204
252
  """
205
- Watch root and every directory under it, except rejected names.
253
+ Install the few recursive watches needed for core and module sources.
206
254
  """
207
- for directory in iter_watch_directories(root):
208
- self._watch_directory(directory)
255
+ for path, recursive in iter_watch_roots():
256
+ self._schedule(path, recursive=recursive)
209
257
 
210
- def _watch_directory(self, path):
258
+ def _schedule(self, path, recursive=True):
211
259
  """
212
- Watch one directory. Rejected names and directories already watched are skipped.
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=False)
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(module=module, frontend=frontend)
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(core=True, frontend=frontend)
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
- module = self._module_name_from_path(created)
402
- if module:
403
- frontend = self.__change_on_frontend(event)
404
- self.logger.debug(' Change on module "%s"' % module)
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 _is_under(created, config.CORE_SRC) or _is_under(
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.logger.debug(" Change on core")
412
- self.actions_executor.note_change(core=True, frontend=frontend)
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
- scheduled = 0
455
- for root in (
456
- config.CORE_SRC,
457
- config.HTML_SRC,
458
- config.BIN_SRC,
459
- config.MODULES_SRC,
460
- ):
461
- before = len(event_handler._watched)
462
- event_handler.schedule_tree(root)
463
- scheduled += len(event_handler._watched) - before
464
-
465
- if not scheduled:
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('Cleep watchdog is running on "%s" (CTRL-C to stop)' % path)
472
- observer.start()
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.44.0
3
+ Version: 1.44.3
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -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