DEV: Introduce `TemporaryRedis` and unset `DISCOURSE_*` env vars in the `themes:isolated_test` rake task (#13401)

DEV: Introduce TemporaryRedis and unset DISCOURSE_* env vars in the themes:isolated_test rake task (#13401)

The themes:isolated_test rake task will now unset all DISCOURSE_* env variables if UNSET_DISCOURSE_ENV_VARS env var is set and will also spin up a temporary redis server so the unicorn web server that’s spun up for the tests doesn’t leak into the “main” redis server.

diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake
index ab902a1..4ffcd5f 100644
--- a/lib/tasks/qunit.rake
+++ b/lib/tasks/qunit.rake
@@ -42,7 +42,10 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
       "UNICORN_PID_PATH" => "#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid", # So this can run alongside development
       "UNICORN_PORT" => port.to_s,
       "UNICORN_SIDEKIQS" => "0",
-      "DISCOURSE_SKIP_CSS_WATCHER" => "1"
+      "DISCOURSE_SKIP_CSS_WATCHER" => "1",
+      "UNICORN_LISTENER" => "127.0.0.1:#{port}",
+      "LOGSTASH_UNICORN_URI" => nil,
+      "UNICORN_WORKERS" => "3"
     },
     "#{Rails.root}/bin/unicorn -c config/unicorn.conf.rb",
     pgroup: true
diff --git a/lib/tasks/themes.rake b/lib/tasks/themes.rake
index f11a628..a69b5e0 100644
--- a/lib/tasks/themes.rake
+++ b/lib/tasks/themes.rake
@@ -120,7 +120,22 @@ task "themes:qunit", :type, :value do |t, args|
 end
 
 desc "Install a theme/component on a temporary DB and run QUnit tests"
-task "themes:install_and_test" => :environment do |t, args|
+task "themes:isolated_test" => :environment do |t, args|
+  # This task can be called in a production environment that likely has a bunch
+  # of DISCOURSE_* env vars that we don't want to be picked up by the Unicorn
+  # server that will be spawned for the tests. So we need to unset them all
+  # before we proceed.
+  # Make this behavior opt-in to make it very obvious.
+  if ENV["UNSET_DISCOURSE_ENV_VARS"] == "1"
+    ENV.keys.each do |key|
+      next if !key.start_with?('DISCOURSE_')
+      ENV[key] = nil
+    end
+  end
+
+  redis = TemporaryRedis.new
+  redis.start
+  $redis = redis.instance # rubocop:disable Style/GlobalVars
   db = TemporaryDb.new
   db.start
   db.migrate
@@ -139,6 +154,7 @@ task "themes:install_and_test" => :environment do |t, args|
   ENV["PGHOST"] = "localhost"
   ENV["QUNIT_RAILS_ENV"] = "development"
   ENV["DISCOURSE_DEV_DB"] = "discourse"
+  ENV["DISCOURSE_REDIS_PORT"] = redis.port.to_s
 
   count = 0
   themes.each do |(name, id)|
@@ -155,4 +171,5 @@ task "themes:install_and_test" => :environment do |t, args|
 ensure
   db&.stop
   db&.remove
+  redis&.remove
 end
diff --git a/lib/temporary_redis.rb b/lib/temporary_redis.rb
new file mode 100644
index 0000000..fb2a799
--- /dev/null
+++ b/lib/temporary_redis.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+class TemporaryRedis
+  REDIS_TEMP_DIR = "/tmp/discourse_temp_redis"
+  REDIS_LOG_PATH = "#{REDIS_TEMP_DIR}/redis.log"
+  REDIS_PID_PATH = "#{REDIS_TEMP_DIR}/redis.pid"
+
+  attr_reader :instance
+
+  def initialize
+    set_redis_server_bin
+  end
+
+  def port
+    @port ||= find_free_port(11000..11900)
+  end
+
+  def start
+    return if @started
+    FileUtils.rm_rf(REDIS_TEMP_DIR)
+    Dir.mkdir(REDIS_TEMP_DIR)
+    FileUtils.touch(REDIS_LOG_PATH)
+
+    puts "Starting redis on port: #{port}"
+    @thread = Thread.new do
+      system(
+        @redis_server_bin,
+        "--port", port.to_s,
+        "--pidfile", REDIS_PID_PATH,
+        "--logfile", REDIS_LOG_PATH,
+        "--databases", "1",
+        "--save", '""',
+        "--appendonly", "no",
+        "--daemonize", "no",
+        "--maxclients", "100",
+        "--dir", REDIS_TEMP_DIR
+      )
+    end
+
+    puts "Waiting for redis server to start..."
+    success = false
+    instance = nil
+    config = {
+      port: port,
+      host: "127.0.0.1",
+      db: 0
+    }
+    start = Time.now
+    while !success
+      begin
+        instance = DiscourseRedis.new(config, namespace: true)
+        success = instance.ping == "PONG"
+      rescue Redis::CannotConnectError
+      ensure
+        if !success && (Time.now - start) >= 5
+          STDERR.puts "ERROR: Could not connect to redis in 5 seconds."
+          self.remove
+          exit(1)
+        elsif !success
+          sleep 0.1
+        end
+      end
+    end
+    puts "Redis is ready"
+    @instance = instance
+    @started = true
+  end
+
+  def remove
+    if @instance
+      @instance.shutdown
+      @thread.join
+      puts "Redis has been shutdown."
+    end
+    FileUtils.rm_rf(REDIS_TEMP_DIR)
+    @started = false
+    puts "Redis files have been cleaned up."
+  end
+
+  private
+
+  def set_redis_server_bin
+    path = `which redis-server 2> /dev/null`.strip
+    if path.size < 1
+      STDERR.puts 'ERROR: redis-server is not installed on this machine. Please install it'
+      exit(1)
+    end
+    @redis_server_bin = path
+  rescue => ex
+    STDERR.puts 'ERROR: Failed to find redis-server binary:'
+    STDERR.puts ex.inspect
+    exit(1)
+  end
+
+  def find_free_port(range)
+    range.each do |port|
+      return port if port_available?(port)
+    end
+  end
+
+  def port_available?(port)
+    TCPServer.open(port).close
+    true
+  rescue Errno::EADDRINUSE
+    false
+  end
+end

GitHub sha: d3a3d1b94cde5b1128f00fb707d8099c835b248c

This commit appears in #13401 which was approved by SamSaffron and tgxworld. It was merged by OsamaSayegh.