watch.t 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. use strict;
  2. use warnings;
  3. use Test2::V0;
  4. use Cwd qw{abs_path};
  5. use File::Path qw{make_path remove_tree};
  6. use File::Temp qw{tempdir};
  7. use File::Basename qw{dirname};
  8. use lib dirname( abs_path(__FILE__) ) . '/../lib';
  9. use TPSGI::Startup;
  10. use TPSGI;
  11. my $tmp = tempdir( CLEANUP => 1 );
  12. # A TPSGI object which is only good for logging & watching, as new() insists on
  13. # being run as the configured user with a full config.
  14. my $tpsgi = bless(
  15. {
  16. ip => '127.0.0.1',
  17. user => 'test',
  18. verbose => 0,
  19. autoreload => 0,
  20. loggers => [],
  21. log_dir => "$tmp/log",
  22. log_name => "$tmp/log/tpsgi.log",
  23. callbacks => [],
  24. },
  25. 'TPSGI'
  26. );
  27. # Give the kernel a moment to queue events, then drain.
  28. sub changes {
  29. select( undef, undef, undef, 0.1 );
  30. return TPSGI::Startup::pending_changes();
  31. }
  32. subtest 'watching a file with a callback' => sub {
  33. my $file = "$tmp/subject.txt";
  34. open( my $fh, '>', $file ) or die $!;
  35. print $fh "initial\n";
  36. close $fh;
  37. my @fired;
  38. my $wd = $tpsgi->add_watch( $file, sub { my ( $t, $change ) = @_; push( @fired, $change ) } );
  39. ok( defined $wd, "add_watch returns a watch descriptor" );
  40. ok( TPSGI::Startup::is_watched($file), "path is now watched" );
  41. open( $fh, '>>', $file ) or die $!;
  42. print $fh "more\n";
  43. close $fh;
  44. $tpsgi->handle_changes();
  45. is( scalar(@fired), 1, "callback fired once for one modification" );
  46. is( $fired[0]{path}, $file, "callback got the path which changed" );
  47. ok( ( grep { $_ eq 'MODIFY' } @{ $fired[0]{events} } ), "callback got decoded event names" );
  48. TPSGI::Startup::remove_watch($file);
  49. ok( !TPSGI::Startup::is_watched($file), "remove_watch drops the path" );
  50. @fired = ();
  51. open( $fh, '>>', $file ) or die $!;
  52. print $fh "even more\n";
  53. close $fh;
  54. $tpsgi->handle_changes();
  55. is( scalar(@fired), 0, "removed watches no longer fire" );
  56. };
  57. subtest 'registering the same callback repeatedly is idempotent' => sub {
  58. my $file = "$tmp/idempotent.txt";
  59. open( my $fh, '>', $file ) or die $!;
  60. close $fh;
  61. my @fired;
  62. my $cb = sub { push( @fired, $_[1] ) };
  63. # new() runs per request, so routers re-register their watches constantly.
  64. $tpsgi->add_watch( $file, $cb ) for 1 .. 5;
  65. my $wd = $TPSGI::Startup::watched{$file};
  66. is( scalar( @{ $TPSGI::Startup::watches{$wd}{callbacks} } ), 1, "callback registered exactly once" );
  67. open( $fh, '>>', $file ) or die $!;
  68. print $fh "poke\n";
  69. close $fh;
  70. $tpsgi->handle_changes();
  71. is( scalar(@fired), 1, "and it only fires once" );
  72. TPSGI::Startup::remove_watch($file);
  73. };
  74. subtest 'distinct callbacks on one path both fire, in order' => sub {
  75. my $file = "$tmp/multi.txt";
  76. open( my $fh, '>', $file ) or die $!;
  77. close $fh;
  78. my @fired;
  79. $tpsgi->add_watch( $file, sub { push( @fired, 'first' ) } );
  80. $tpsgi->add_watch( $file, sub { push( @fired, 'second' ) } );
  81. open( $fh, '>>', $file ) or die $!;
  82. print $fh "poke\n";
  83. close $fh;
  84. $tpsgi->handle_changes();
  85. is( \@fired, [qw{first second}], "both callbacks fired in registration order" );
  86. TPSGI::Startup::remove_watch($file);
  87. };
  88. subtest 'watching a directory reports the entries which changed' => sub {
  89. my $dir = "$tmp/watched_dir";
  90. make_path($dir);
  91. my @fired;
  92. $tpsgi->add_watch( $dir, sub { push( @fired, $_[1] ) } );
  93. open( my $fh, '>', "$dir/created.txt" ) or die $!;
  94. close $fh;
  95. $tpsgi->handle_changes();
  96. ok( scalar(@fired), "got at least one change for the directory" );
  97. is( $fired[0]{path}, "$dir/created.txt", "change path is the full path of the entry" );
  98. is( $fired[0]{watched}, $dir, "change knows which watch it came from" );
  99. is( $fired[0]{name}, 'created.txt', "and the raw inotify name" );
  100. TPSGI::Startup::remove_watch($dir);
  101. };
  102. subtest 'watches survive a file being replaced wholesale' => sub {
  103. my $file = "$tmp/renamed.txt";
  104. open( my $fh, '>', $file ) or die $!;
  105. print $fh "one\n";
  106. close $fh;
  107. my @fired;
  108. my $original_wd = $tpsgi->add_watch( $file, sub { push( @fired, $_[1] ) } );
  109. # The write-a-tempfile-then-rename-over-it dance, which kills the old watch.
  110. open( $fh, '>', "$tmp/renamed.tmp" ) or die $!;
  111. print $fh "two\n";
  112. close $fh;
  113. rename( "$tmp/renamed.tmp", $file ) or die $!;
  114. $tpsgi->handle_changes();
  115. ok( scalar(@fired), "got the change which clobbered the file" );
  116. my $new_wd = $TPSGI::Startup::watched{$file};
  117. ok( defined $new_wd, "path is still watched afterwards" );
  118. is( scalar( @{ $TPSGI::Startup::watches{$new_wd}{callbacks} } ), 1, "callback survived the re-watch" );
  119. @fired = ();
  120. open( $fh, '>>', $file ) or die $!;
  121. print $fh "three\n";
  122. close $fh;
  123. $tpsgi->handle_changes();
  124. is( scalar(@fired), 1, "and still fires for changes to the new inode" );
  125. TPSGI::Startup::remove_watch($file);
  126. };
  127. subtest 'a dying callback does not take out the request' => sub {
  128. my $file = "$tmp/exploding.txt";
  129. open( my $fh, '>', $file ) or die $!;
  130. close $fh;
  131. my @fired;
  132. $tpsgi->add_watch( $file, sub { die "nope\n" }, key => 'boom' );
  133. $tpsgi->add_watch( $file, sub { push( @fired, 'ran' ) }, key => 'fine' );
  134. open( $fh, '>>', $file ) or die $!;
  135. print $fh "poke\n";
  136. close $fh;
  137. ok( lives { $tpsgi->handle_changes() }, "handle_changes survives a dying callback" );
  138. is( \@fired, ['ran'], "and still runs the other callbacks" );
  139. TPSGI::Startup::remove_watch($file);
  140. };
  141. subtest 'watching something which is not there is not fatal' => sub {
  142. my $wd;
  143. ok(
  144. lives {
  145. $wd = $tpsgi->add_watch( "$tmp/does_not_exist", sub { } )
  146. },
  147. "add_watch on a missing path lives"
  148. );
  149. is( $wd, undef, "and reports failure" );
  150. ok( !TPSGI::Startup::is_watched("$tmp/does_not_exist"), "nothing got registered" );
  151. };
  152. subtest 'libdir watches still drive autoreload' => sub {
  153. my $libdir = "$tmp/lib";
  154. make_path($libdir);
  155. open( my $fh, '>', "$libdir/Fake.pm" ) or die $!;
  156. print $fh "1;\n";
  157. close $fh;
  158. TPSGI::Startup::watch_for_changes($libdir);
  159. my $restarted = 0;
  160. no warnings qw{redefine once};
  161. local *TPSGI::signal_restart_parent = sub { $restarted++ };
  162. use warnings;
  163. open( $fh, '>>', "$libdir/Fake.pm" ) or die $!;
  164. print $fh "# poke\n";
  165. close $fh;
  166. local $tpsgi->{autoreload} = 1;
  167. $tpsgi->handle_changes();
  168. ok( $restarted, "a changed .pm in a libdir signals a restart" );
  169. # And the app-facing watches don't get roped into that.
  170. $restarted = 0;
  171. my $data = "$tmp/data.pm";
  172. open( $fh, '>', $data ) or die $!;
  173. close $fh;
  174. $tpsgi->add_watch( $data, sub { } );
  175. open( $fh, '>>', $data ) or die $!;
  176. print $fh "poke\n";
  177. close $fh;
  178. $tpsgi->handle_changes();
  179. ok( !$restarted, "but a .pm watched by an app does not" );
  180. TPSGI::Startup::remove_watch($data);
  181. };
  182. done_testing();