watch.t 7.5 KB

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