From 55c832622d641b3b008803ef3d268e1e4839f2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 20 Apr 2012 12:39:28 +0200 Subject: [PATCH] "watch": Support same file in multiple bundles. There are now also some tests for the watch command. Closes #127. --- src/webassets/script.py | 22 ++++++++--- tests/test_script.py | 88 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/webassets/script.py b/src/webassets/script.py index 9032315d..b94c2d8e 100644 --- a/src/webassets/script.py +++ b/src/webassets/script.py @@ -199,14 +199,22 @@ def build(self, bundles=None, output=None, directory=None, no_cache=None, if len(built): self.event_handlers['post_build']() - def watch(self): + def watch(self, loop=None): """Watch assets for changes. - TODO: This should probably also restart when the code changes. + ``loop`` + A callback, taking no arguments, to be called once every loop + iteration. Can be useful to integrate the command with other code. + If not specified, the loop wil call ``time.sleep()``. """ + # TODO: This should probably also restart when the code changes. _mtimes = {} _win = (sys.platform == "win32") def check_for_changes(): + # Do not update original mtimes dict right away, so that we detect + # all bundle changes if a file is in multiple bundles. + _new_mtimes = _mtimes.copy() + changed_bundles = [] for bundle in self.environment: for filename in get_all_bundle_files(bundle): @@ -217,9 +225,11 @@ def check_for_changes(): if _mtimes.get(filename, mtime) != mtime: changed_bundles.append(bundle) - _mtimes[filename] = mtime + _new_mtimes[filename] = mtime break - _mtimes[filename] = mtime + _new_mtimes[filename] = mtime + + _mtimes.update(_new_mtimes) return changed_bundles try: @@ -236,7 +246,9 @@ def check_for_changes(): print "Failed: %s" % e if len(built): self.event_handlers['post_build']() - time.sleep(0.1) + do_end = loop() if loop else time.sleep(0.1) + if do_end: + break except KeyboardInterrupt: pass diff --git a/tests/test_script.py b/tests/test_script.py index 7c8dde4c..81075742 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -6,8 +6,11 @@ from __future__ import with_statement +from os import path import logging +from threading import Thread, Event from nose.tools import assert_raises +import time from webassets import Bundle from webassets.script import main, CommandLineEnvironment, CommandError from webassets.test import TempEnvironmentHelper @@ -137,3 +140,88 @@ def test_manifest(self): # Use prefix syntax self.cmd_env.build(manifest='file:miau') assert self.exists('media/sub/miau') + + +class TestWatchCommand(TestCLI): + """This is a hard one to test. + + We run the watch command in a thread, and rely on it's ``loop`` argument + to stop the thread again. + """ + + default_files = {'in': 'foo', 'out': 'bar'} + + def watch_loop(self): + # Hooked into the loop of the ``watch`` command. + # Allows stopping the thread. + self.has_looped.set() + time.sleep(0.01) + if getattr(self, 'stopped', False): + return True + + def start_watching(self): + """Run the watch command in a thread.""" + self.has_looped = Event() + t = Thread(target=self.cmd_env.watch, kwargs={'loop': self.watch_loop}) + t.daemon = True # In case something goes wrong with stopping, this + # will allow the test process to be end nonetheless. + t.start() + self.t = t + # Wait for first iteration, which will initialize the mtimes. Only + # after this will ``watch`` be able to detect changes. + self.has_looped.wait(1) + + def stop_watching(self): + """Stop the watch command thread.""" + self.stopped = True + self.t.join(1) + + def __enter__(self): + self.start_watching() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop_watching() + + def test(self): + # Register a bundle to watch + bundle = self.mkbundle('in', output='out') + self.env.register('test', bundle) + now = self.setmtime('in', 'out') + + # Assert initial state + assert self.get('out') == 'bar' + + # While watch is running, change input mtime + with self: + self.setmtime('in', mtime=now+10) + # Allow watch to pick up the change + time.sleep(0.2) + + # output file has been updated. + assert self.get('out') == 'foo' + + + def test_same_file_multiple_bundles(self): + """[Bug] Test watch command can deal with the same file being part + of multiple bundles. This was not always the case (github-127). + """ + self.create_files({'out2': 'bar'}) + bundle1 = self.mkbundle('in', output='out') + bundle2 = self.mkbundle('in', output='out2') + self.env.register('test1', bundle1) + self.env.register('test2', bundle2) + now = self.setmtime('in', 'out', 'out2') + + # Assert initial state + assert self.get('out') == 'bar' + assert self.get('out2') == 'bar' + + # While watch is running, change input mtime + with self: + self.setmtime('in', mtime=now+10) + # Allow watch to pick up the change + time.sleep(0.2) + + # Both output files have been updated. + assert self.get('out') == 'foo' + assert self.get('out2') == 'foo'