diff --git a/Changes b/Changes index 1ce2695..2d60b00 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ Revision history for Perl extension Mac::FSEvents. +0.08 2012-05-28 14:00:00 + - fix broken MANIFEST + 0.07 2012-05-21 19:15:00 - Allow users to specify creation flags for FSEvents. - Fix the compilation option detection process. diff --git a/MANIFEST b/MANIFEST index cd525a6..db31733 100644 --- a/MANIFEST +++ b/MANIFEST @@ -3,6 +3,7 @@ FSEvents.xs hints/darwin.pl lib/Mac/FSEvents.pm lib/Mac/FSEvents/Event.pm +MacVersion.pm Makefile.PL MANIFEST ppport.h @@ -13,6 +14,11 @@ t/03podcoverage.t t/04critic.rc t/04critic.t t/05event.t +t/06flags.t +t/07noflags.t +t/08leftover-events.t +t/09subprocess-events.t +t/10receive-all-changes.t typemap META.yml Module meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) diff --git a/META.json b/META.json index 8cd7e62..0765f19 100644 --- a/META.json +++ b/META.json @@ -1,7 +1,8 @@ { "abstract" : "Monitor a directory structure for changes", "author" : [ - "Andy Grundman " + "Andy Grundman ", + "Rob Hoelz `" ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 6.6302, CPAN::Meta::Converter version 2.120630", @@ -37,5 +38,5 @@ } }, "release_status" : "stable", - "version" : "0.07" + "version" : "0.08" } diff --git a/META.yml b/META.yml index a21fdbd..3024d80 100644 --- a/META.yml +++ b/META.yml @@ -2,6 +2,7 @@ abstract: 'Monitor a directory structure for changes' author: - 'Andy Grundman ' + - 'Rob Hoelz `' build_requires: ExtUtils::MakeMaker: 0 configure_requires: @@ -19,4 +20,4 @@ no_index: - inc requires: File::Slurp: 0 -version: 0.07 +version: 0.08 diff --git a/MacVersion.pm b/MacVersion.pm new file mode 100644 index 0000000..16849a2 --- /dev/null +++ b/MacVersion.pm @@ -0,0 +1,30 @@ +package + MacVersion; + +use strict; +use warnings; +use base 'Exporter'; + +our @EXPORT = qw(osx_version); + +sub osx_version { + my $os_version = qx(system_profiler SPSoftwareDataType); + if($os_version =~ /System Version: Mac OS X (?:Server )?(10\.\d+)/) { + return $1; + } else { + $os_version =~ s/^/> /gm; + + die <<"END_DIE"; +Could not parse version string! +Please file a bug report on CPAN, and include the following +in the description: + +$os_version +END_DIE + + exit 1; + } + +} + +1; diff --git a/Makefile.PL b/Makefile.PL index b25f2d1..c44fcb3 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -10,7 +10,7 @@ WriteMakefile( VERSION_FROM => 'lib/Mac/FSEvents.pm', PREREQ_PM => {}, ABSTRACT_FROM => 'lib/Mac/FSEvents.pm', - AUTHOR => 'Andy Grundman ', + AUTHOR => [ 'Andy Grundman ', 'Rob Hoelz `' ], LIBS => [''], LDDLFLAGS => $Config{lddlflags} . ' -framework CoreServices -framework CoreFoundation', DEFINE => '', diff --git a/lib/Mac/FSEvents.pm b/lib/Mac/FSEvents.pm index 3bcbc5e..490ca94 100644 --- a/lib/Mac/FSEvents.pm +++ b/lib/Mac/FSEvents.pm @@ -6,7 +6,7 @@ use base 'Exporter'; use Mac::FSEvents::Event; -our $VERSION = '0.07'; +our $VERSION = '0.08'; our @EXPORT_OK = qw(NONE WATCH_ROOT); our %EXPORT_TAGS = ( flags => \@EXPORT_OK ); diff --git a/t/06flags.t b/t/06flags.t new file mode 100644 index 0000000..c21ce32 --- /dev/null +++ b/t/06flags.t @@ -0,0 +1,214 @@ +use strict; +use warnings; +use autodie; + +use Carp qw(croak); +use Cwd qw(getcwd); +use FindBin; +use File::Path qw(make_path rmtree); +use File::Spec; +use File::Temp; +use IO::Select; +use Mac::FSEvents qw(:flags); +use Time::HiRes qw(usleep); + +use Test::More tests => 5; + +my %capable_of; + +BEGIN { + foreach my $constant ( qw{IGNORE_SELF FILE_EVENTS} ) { + if(__PACKAGE__->can($constant)) { + $capable_of{$constant} = 1; + } else { + no strict 'refs'; + + *$constant = sub { + return 0; + }; + } + } +} + +my $TEST_LATENCY = 0.5; +my $TIMEOUT = 3; + +my $tmpdir = "$FindBin::Bin/tmp"; + +sub touch_file { + my ( $filename ) = @_; + + my $fh; + open $fh, '>', $filename or die $!; + close $fh; + + return; +} + +sub reset_fs { + rmtree $tmpdir if -d $tmpdir; + + mkdir $tmpdir; +} + +sub with_wd (&$) { + my ( $callback, $dir ) = @_; + + my $wd = getcwd(); + + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my $ok = eval { + chdir $dir or croak $!; + + $callback->(); + 1; + }; + my $error = $@; + chdir $wd; + die $error unless $ok; + + return; +} + +sub dissect_event { + my ( $event ) = @_; + + return { + path => $event->path, + }; +} + +sub fetch_events { + my ( $fs, $fh ) = @_; + + my @events; + + my $sel = IO::Select->new($fh); + + while( $sel->can_read($TIMEOUT) ) { + foreach my $event ( $fs->read_events ) { + push @events, $event; + } + } + + return @events; +} + +sub normalize_event { + my ( $event ) = @_; + + my $path; + + if(ref($event) eq 'Mac::FSEvents::Event') { + $path = $event->path; + } else { + $path = $event->{'path'}; + } + $event = {}; + + $event->{'path'} = File::Spec->canonpath($path); + + return $event; +} + +sub cmp_events { + my ( $lhs, $rhs ) = @_; + + local $Test::Builder::Level = $Test::Builder::Level + 1; + + foreach my $event (@$lhs, @$rhs) { + $event = normalize_event($event); + } + + return is_deeply($lhs, $rhs); +}; + +sub test_flags { + my ( $flags, $create_files, $expected_events ) = @_; + + reset_fs(); + + local $Test::Builder::Level = $Test::Builder::Level + 1; + + sleep 1; # wait for reset_fs triggered events to pass + + my $fs = Mac::FSEvents->new({ + path => $tmpdir, + latency => $TEST_LATENCY, + flags => $flags, + }); + + my $fh = $fs->watch; + + with_wd { + $create_files->(); + } $tmpdir; + + my @events = map { normalize_event($_) } fetch_events($fs, $fh); + + $fs->stop; + + return cmp_events \@events, $expected_events; +} + +sub test_watch_root { + reset_fs(); + + local $Test::Builder::Level = $Test::Builder::Level + 1; + + with_wd { + make_path('foo/bar'); + + my $fs = Mac::FSEvents->new({ + path => 'foo/bar', + latency => $TEST_LATENCY, + flags => WATCH_ROOT, + }); + + my $fh = $fs->watch; + + usleep 100_000; # XXX wait a little for watcher to catch up; + # this is a bug that I'll fix! + + rename 'foo/bar', 'foo/baz' or die $!; + + my @events = fetch_events($fs, $fh); + + is scalar(@events), 1; + ok $events[0]->root_changed; + } $tmpdir; +} + +test_flags(NONE, sub { + touch_file 'foo.txt'; + touch_file 'bar.txt'; +}, [ + { path => $tmpdir }, # one event, because it's coalesced +]); + +test_watch_root(); + +SKIP: { + skip q{Your platform doesn't support IGNORE_SELF}, 1 unless($capable_of{'IGNORE_SELF'}); + + test_flags(IGNORE_SELF, sub { + mkdir 'foo'; + + system 'touch foo/bar.txt'; + }, [ + { path => "$tmpdir/foo" }, + ]); +} + +SKIP: { + skip q{Your platform doesn't support FILE_EVENTS}, 1 unless $capable_of{'FILE_EVENTS'}; + + test_flags(FILE_EVENTS, sub { + touch_file 'foo.txt'; + touch_file 'bar.txt'; + }, [ + { path => "$tmpdir/foo.txt" }, + { path => "$tmpdir/bar.txt" }, + ]); +} diff --git a/t/07noflags.t b/t/07noflags.t new file mode 100644 index 0000000..aa28eb5 --- /dev/null +++ b/t/07noflags.t @@ -0,0 +1,18 @@ +use strict; +use warnings; + +use Mac::FSEvents; +use Test::More; + +my @FLAGS = qw{ + NONE + WATCH_ROOT + IGNORE_SELF + FILE_EVENTS +}; + +plan tests => scalar(@FLAGS); + +foreach my $flag (@FLAGS) { + ok !__PACKAGE__->can($flag), 'flags should not be imported unless :flags is specified'; +} diff --git a/t/08leftover-events.t b/t/08leftover-events.t new file mode 100644 index 0000000..d0ef9c5 --- /dev/null +++ b/t/08leftover-events.t @@ -0,0 +1,43 @@ +use strict; +use warnings; + +use File::Temp; +use IO::Select; +use Mac::FSEvents; + +use Test::More skip_all => 'This is a problem with FSEvents itself, it seems'; + +my $LATENCY = 0.5; +my $TIMEOUT = 1.0; + +my $dir = File::Temp->newdir; + +my $fs = Mac::FSEvents->new({ + path => $dir->dirname, + latency => $LATENCY, +}); + +my $fh = $fs->watch; +my $sel = IO::Select->new($fh); + +mkdir "$dir/foo"; + +my $has_events = $sel->can_read($TIMEOUT); +ok $has_events, q{we should have an event to process the first time around}; + +$fs->stop; + +undef $fs; + +rmdir "$dir/foo"; + +$fs = Mac::FSEvents->new({ + path => $dir->dirname, + latency => $LATENCY, +}); + +$fh = $fs->watch; +$sel = IO::Select->new($fh); +$has_events = $sel->can_read($TIMEOUT); + +ok !$has_events, q{a new watcher shouldn't receive old events}; diff --git a/t/09subprocess-events.t b/t/09subprocess-events.t new file mode 100644 index 0000000..963e9b4 --- /dev/null +++ b/t/09subprocess-events.t @@ -0,0 +1,62 @@ +use strict; +use warnings; +use autodie; + +use IO::Select; +use File::Path qw(remove_tree); +use File::Temp; +use Mac::FSEvents; + +use Test::More tests => 2; + +sub touch_file { + my ( $filename ) = @_; + + my $fh; + open $fh, '>', $filename; + close $fh; + + return; +} + +sub subprocess (&) { + my ( $action ) = @_; + + my $pid = fork; + + if($pid) { + waitpid $pid, 0; + } else { + eval { + $action->(); + }; + exit 0; + } +} + +my $LATENCY = 0.5; +my $TIMEOUT = 1.0; + +my $dir = File::Temp->newdir; + +my $fs = Mac::FSEvents->new({ + path => $dir->dirname, + latency => $LATENCY, +}); +my $fh = $fs->watch; +my $sel = IO::Select->new($fh); + +# our subprocess will call DESTROY on the Mac::FSEvents object! +my $pid = fork; +unless($pid) { + exit 0; +} +waitpid $pid, 0; + +touch_file "$dir/foo.txt"; + +my $has_events = $sel->can_read($TIMEOUT); +ok $has_events; + +my @events = $fs->read_events; +is scalar(@events), 1; diff --git a/t/10receive-all-changes.t b/t/10receive-all-changes.t new file mode 100644 index 0000000..e6d65d4 --- /dev/null +++ b/t/10receive-all-changes.t @@ -0,0 +1,78 @@ +use strict; +use warnings; + +use File::Path qw(remove_tree); +use File::Slurp qw(write_file); +use File::Spec; +use Mac::FSEvents qw(:flags); +use Test::More; + +BEGIN { + unless(__PACKAGE__->can('FILE_EVENTS')) { + plan skip_all => 'OS X 10.7 or greater needed for this test'; + exit 0; + } +} + +plan tests => 1; + +my $LATENCY = 0.5; +my $TIMEOUT = 120; +my $EXPECTED_EVENTS = 10_000; + +sub is_same_file { + my ( $lhs, $rhs ) = @_; + + my ( $lhs_dev, $lhs_inode ) = (stat $lhs)[0, 1]; + my ( $rhs_dev, $rhs_inode ) = (stat $rhs)[0, 1]; + + return $lhs_dev == $rhs_dev && $lhs_inode == $rhs_inode; +} + +my $tmpdir = File::Spec->rel2abs('tmp'); + +remove_tree($tmpdir); +mkdir $tmpdir; + +sleep 2; # make sure we don't receive an event for creating our tmpdir + +my $fsevents = Mac::FSEvents->new({ + path => $tmpdir, + latency => $LATENCY, + flags => FILE_EVENTS, +}); + +$fsevents->watch; + +sleep 2; # make sure we don't receive an event for creating our tmpdir + +my $event_count = 0; + +for my $n ( 1 .. $EXPECTED_EVENTS) { + write_file(File::Spec->catfile($tmpdir, $n), 'foobar'); +} + +$SIG{'ALRM'} = sub { die "alarm" }; + +alarm $TIMEOUT; + +eval { +EVENT_LOOP: + while(my @events = $fsevents->read_events) { + foreach my $e (@events) { + my $path = $e->path; + my ( undef, $dir ) = File::Spec->splitpath($path); + + if(is_same_file($dir, $tmpdir)) { + $event_count++; + last EVENT_LOOP if $event_count >= $EXPECTED_EVENTS; + } + } + } +}; + +if($@ && $@ !~ /alarm/) { + die $@; +} + +is $event_count, $EXPECTED_EVENTS, 'every event should be seen';