# frozen_string_literal: true require_relative "abstract_unit" require "pathname" require "weakref" require_relative "file_update_checker_shared_tests" class EventedFileUpdateCheckerTest < ActiveSupport::TestCase include FileUpdateCheckerSharedTests def setup skip if ENV["LISTEN"] == "0" require "listen" super end def new_checker(files = [], dirs = {}, &block) ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c| wait end end def teardown super Listen.stop end def wait sleep 0.5 end def mkdir(dirs) super wait # wait for the events to fire end def touch(files) super wait # wait for the events to fire end def rm_f(files) super wait # wait for the events to fire end test "notifies forked processes" do skip "Forking not available" unless Process.respond_to?(:fork) FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) { } assert_not_predicate checker, :updated? # Pipes used for flow control across fork. boot_reader, boot_writer = IO.pipe touch_reader, touch_writer = IO.pipe result_reader, result_writer = IO.pipe pid = fork do assert_not_predicate checker, :updated? # The listen gem start multiple background threads that need to reach a ready state. # Unfortunately, it doesn't look like there is a clean way to block until they are ready. wait # Fork is booted, ready for file to be touched # notify parent process. boot_writer.write("booted") # Wait for parent process to signal that file # has been touched. IO.select([touch_reader]) assert_predicate checker, :updated? rescue Exception => ex result_writer.write("#{ex.class.name}: #{ex.message}") raise ensure result_writer.close end assert pid result_writer.close # Wait for fork to be booted before touching files. IO.select([boot_reader]) touch(tmpfiles) # Notify fork that files have been touched. touch_writer.write("touched") assert_predicate checker, :updated? Process.wait(pid) assert_equal "", result_reader.read end test "can be garbage collected" do # Use a separate thread to isolate objects and ensure they will be garbage collected. checker_ref, listener_threads = Thread.new do threads_before_checker = Thread.list checker = ActiveSupport::EventedFileUpdateChecker.new([], tmpdir => ".rb") { } # Wait for listener thread to start processing events. wait [WeakRef.new(checker), Thread.list - threads_before_checker] end.value GC.start listener_threads.each { |t| t.join(1) } assert_not checker_ref.weakref_alive?, "EventedFileUpdateChecker was not garbage collected" assert_empty Thread.list & listener_threads end test "should detect changes through symlink" do actual_dir = File.join(tmpdir, "actual") linked_dir = File.join(tmpdir, "linked") Dir.mkdir(actual_dir) FileUtils.ln_s(actual_dir, linked_dir) checker = new_checker([], linked_dir => ".rb") { } assert_not_predicate checker, :updated? touch(File.join(actual_dir, "a.rb")) assert_predicate checker, :updated? assert checker.execute_if_updated end test "updated should become true when nonexistent directory is added later" do watched_dir = File.join(tmpdir, "app") unwatched_dir = File.join(tmpdir, "node_modules") not_exist_watched_dir = File.join(tmpdir, "test") Dir.mkdir(watched_dir) Dir.mkdir(unwatched_dir) checker = new_checker([], watched_dir => ".rb", not_exist_watched_dir => ".rb") { } touch(File.join(watched_dir, "a.rb")) assert_predicate checker, :updated? assert checker.execute_if_updated Dir.mkdir(not_exist_watched_dir) wait assert_predicate checker, :updated? assert checker.execute_if_updated touch(File.join(unwatched_dir, "a.rb")) assert_not_predicate checker, :updated? assert_not checker.execute_if_updated end test "does not stop other checkers when nonexistent directory is added later" do dir1 = File.join(tmpdir, "app") dir2 = File.join(tmpdir, "test") Dir.mkdir(dir2) checker1 = new_checker([], dir1 => ".rb") { } checker2 = new_checker([], dir2 => ".rb") { } Dir.mkdir(dir1) touch(File.join(dir1, "a.rb")) assert_predicate checker1, :updated? assert_not_predicate checker2, :updated? touch(File.join(dir2, "a.rb")) assert_predicate checker2, :updated? end end