discourse/lib/autospec/manager.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

386 lines
9.8 KiB
Ruby

# frozen_string_literal: true
require "listen"
require "thread"
require "fileutils"
require "autospec/reload_css"
require "autospec/base_runner"
require "socket_server"
module Autospec; end
class Autospec::Manager
def self.run(opts = {})
self.new(opts).run
end
def initialize(opts = {})
@opts = opts
@debug = opts[:debug]
@queue = []
@mutex = Mutex.new
@signal = ConditionVariable.new
@runners = [ruby_runner]
if ENV["QUNIT"] == "1"
@runners << javascript_runner
else
puts "Skipping JS tests, run them in the browser at /qunit or add QUNIT=1 to env"
end
end
def run
Signal.trap("HUP") { stop_runners; exit }
Signal.trap("INT") do
begin
stop_runners
rescue => e
puts "FAILED TO STOP RUNNERS #{e}"
end
exit
end
ensure_all_specs_will_run
start_runners
start_service_queue
listen_for_changes
puts "Press [ENTER] to stop the current run"
while @runners.any?(&:running?)
STDIN.gets
process_queue
end
rescue => e
fail(e, "failed in run")
ensure
stop_runners
end
private
def ruby_runner
require "autospec/simple_runner"
Autospec::SimpleRunner.new
end
def javascript_runner
require "autospec/qunit_runner"
Autospec::QunitRunner.new
end
def ensure_all_specs_will_run(current_runner = nil)
puts "@@@@@@@@@@@@ ensure_all_specs_will_run" if @debug
@queue.reject! { |_, s, _| s == "spec" }
if current_runner
@queue.concat [['spec', 'spec', current_runner]]
end
@runners.each do |runner|
@queue.concat [['spec', 'spec', runner]] unless @queue.any? { |_, s, r| s == "spec" && r == runner }
end
end
[:start, :stop, :abort].each do |verb|
define_method("#{verb}_runners") do
puts "@@@@@@@@@@@@ #{verb}_runners" if @debug
@runners.each(&verb)
end
end
def start_service_queue
puts "@@@@@@@@@@@@ start_service_queue" if @debug
Thread.new do
while true
thread_loop
end
end
end
# the main loop, will run the specs in the queue till one fails or the queue is empty
def thread_loop
puts "@@@@@@@@@@@@ thread_loop" if @debug
@mutex.synchronize do
current = @queue.first
last_failed = false
last_failed = process_spec(current) if current
# stop & wait for the queue to have at least one item or when there's been a failure
if @debug
puts "@@@@@@@@@@@@ waiting because..."
puts "@@@@@@@@@@@@ ...current spec has failed" if last_failed
puts "@@@@@@@@@@@@ ...queue is empty" if @queue.length == 0
end
@signal.wait(@mutex) if @queue.length == 0 || last_failed
end
rescue => e
fail(e, "failed in main loop")
end
# will actually run the spec and check whether the spec has failed or not
def process_spec(current)
puts "@@@@@@@@@@@@ process_spec --> #{current}" if @debug
has_failed = false
# retrieve the instance of the runner
runner = current[2]
# actually run the spec (blocking call)
result = runner.run(current[1]).to_i
if result == 0
puts "@@@@@@@@@@@@ success" if @debug
# remove the spec from the queue
@queue.shift
else
puts "@@@@@@@@@@@@ failure" if @debug
has_failed = true
if result > 0
focus_on_failed_tests(current)
ensure_all_specs_will_run(runner)
end
end
has_failed
end
def focus_on_failed_tests(current)
puts "@@@@@@@@@@@@ focus_on_failed_tests --> #{current}" if @debug
runner = current[2]
# we only want 1 focus in the queue
@queue.shift if current[0] == "focus"
# focus on the first 10 failed specs
failed_specs = runner.failed_specs[0..10]
puts "@@@@@@@@@@@@ failed_specs --> #{failed_specs}" if @debug
# try focus tag
if failed_specs.length > 0
filename, _ = failed_specs[0].split(":")
if filename && File.exist?(filename) && !File.directory?(filename)
spec = File.read(filename)
start, _ = spec.split(/\S*#focus\S*$/)
if start.length < spec.length
line = start.scan(/\n/).length + 1
puts "Found #focus tag on line #{line}!"
failed_specs = ["#{filename}:#{line + 1}"]
end
end
end
# focus on the failed specs
@queue.unshift ["focus", failed_specs.join(" "), runner] if failed_specs.length > 0
end
def root_path
root_path ||= File.expand_path(File.dirname(__FILE__) + "../../..")
end
def reverse_symlink_map
map = {}
Dir[root_path + "/plugins/*"].each do |f|
next if !File.directory? f
resolved = File.realpath(f)
if resolved != f
map[resolved] = f
end
end
map
end
# plugins can be symlinked, try to figure out which plugin this is
def reverse_symlink(file)
resolved = file
@reverse_map ||= reverse_symlink_map
@reverse_map.each do |location, discourse_location|
if file.start_with?(location)
resolved = discourse_location + file[location.length..-1]
end
end
resolved
end
def listen_for_changes
puts "@@@@@@@@@@@@ listen_for_changes" if @debug
options = {
ignore: /^lib\/autospec/,
}
if @opts[:force_polling]
options[:force_polling] = true
options[:latency] = @opts[:latency] || 3
end
path = root_path
if ENV['VIM_AUTOSPEC']
STDERR.puts "Using VIM file listener"
socket_path = (Rails.root + "tmp/file_change.sock").to_s
FileUtils.rm_f(socket_path)
server = SocketServer.new(socket_path)
server.start do |line|
file, line = line.split(' ')
file = reverse_symlink(file)
file = file.sub(Rails.root.to_s + "/", "")
# process_change can aquire a mutex and block
# the acceptor
Thread.new do
if file =~ /(es6|js)$/
process_change([[file]])
else
process_change([[file, line]])
end
end
"OK"
end
return
end
# to speed up boot we use a thread
["spec", "lib", "app", "config", "test", "vendor", "plugins"].each do |watch|
puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug
Thread.new do
begin
listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _|
paths = [modified, added].flatten
paths.compact!
paths.map! do |long|
long = reverse_symlink(long)
long[(path.length + 1)..-1]
end
process_change(paths)
end
listener.start
sleep
rescue => e
puts "FAILED to listen on changes to #{path}/#{watch}"
puts e
end
end
end
end
def process_change(files)
return if files.length == 0
puts "@@@@@@@@@@@@ process_change --> #{files}" if @debug
specs = []
hit = false
files.each do |file, line|
@runners.each do |runner|
# reloaders
runner.reloaders.each do |k|
if k.match(file)
puts "@@@@@@@@@@@@ #{file} matched a reloader for #{runner}" if @debug
runner.reload
return
end
end
# watchers
runner.watchers.each do |k, v|
if m = k.match(file)
puts "@@@@@@@@@@@@ #{file} matched a watcher for #{runner}" if @debug
hit = true
spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
with_line = spec
if spec == file && line
with_line = spec + ":" << line.to_s
end
if File.exists?(spec) || Dir.exists?(spec)
if with_line != spec
specs << [file, spec, runner]
end
specs << [file, with_line, runner]
end
end
end
end
end
queue_specs(specs) if hit
rescue => e
fail(e, "failed in watcher")
end
def queue_specs(specs)
puts "@@@@@@@@@@@@ queue_specs --> #{specs}" if @debug
if specs.length == 0
locked = @mutex.try_lock
if locked
@signal.signal
@mutex.unlock
end
return
else
abort_runners
end
puts "@@@@@@@@@@@@ waiting for the mutex" if @debug
@mutex.synchronize do
puts "@@@@@@@@@@@@ queueing specs" if @debug
puts "@@@@@@@@@@@@ #{@queue}" if @debug
specs.each do |file, spec, runner|
# make sure there's no other instance of this spec in the queue
@queue.delete_if { |_, s, r| s.strip.start_with?(spec.strip) && r == runner }
# deal with focused specs
if @queue.first && @queue.first[0] == "focus"
focus = @queue.shift
@queue.unshift([file, spec, runner])
unless spec.include?(":") && focus[1].include?(spec.split(":")[0])
if focus[1].include?(spec) || file != spec
@queue.unshift(focus)
end
end
else
@queue.unshift([file, spec, runner])
end
# push run all specs to end of queue in correct order
ensure_all_specs_will_run(runner)
end
puts "@@@@@@@@@@@@ specs queued" if @debug
puts "@@@@@@@@@@@@ #{@queue}" if @debug
@signal.signal
end
end
def process_queue
puts "@@@@@@@@@@@@ process_queue" if @debug
if @queue.length == 0
puts "@@@@@@@@@@@@ queue is empty..." if @debug
ensure_all_specs_will_run
@signal.signal
else
current = @queue.first
runner = current[2]
specs = runner.failed_specs
puts
puts
if specs.length == 0
puts "No specs have failed yet! "
puts
else
puts "The following specs have failed:"
specs.each { |s| puts s }
puts
specs = specs.map { |s| [s, s, runner] }
queue_specs(specs)
end
end
end
def fail(exception, message = nil)
puts message if message
puts exception.message
puts exception.backtrace.join("\n")
end
end