rspec-daemonできるか?
結論
ここまででサンドボックス環境では問題なく動くようにできた。
しかし現実的にrails環境で外部設定ファイルとかちゃんと読むのか?などの不安はある。
bundle exec rspec
だと無指定で全ファイル実行するがこれはそうならないし、何か考慮すべき前処理が抜けてる可能性は高い。初期化とか。
そのあたりのdeep diveはまた今度。初期実験としてはいい感じ。
最終版スクリプト
追記: 2023/4/11
簡易的に下記コードをgist化 & MITライセンスを明記しました。LICENSEファイルがある以外は同じコードになっています。
# frozen_string_literal: true
require 'socket'
require 'stringio'
SCRIPT_NAME = File.basename(__FILE__).freeze
def entry_point
RSpec::Core::Runner.disable_autorun!
server = TCPServer.open('0.0.0.0', 3002)
loop do
handle_request(server.accept)
rescue Interrupt
puts 'quit'
server.close
break
end
end
def handle_request(socket)
status, out = run(socket.read)
socket.puts(status)
socket.puts(out)
socket.puts(__FILE__)
rescue StandardError => e
socket.puts e.full_message
ensure
socket.close
end
def run(msg, options = [])
options += ['--force-color']
argv = msg.split(' ')
RSpec.reset
out = StringIO.new
status = RSpec::Core::Runner.run(options + argv, out, out)
[status, out.string]
end
RSpec::Core::BacktraceFormatter.class_eval do
def format_backtrace(backtrace, options = {})
return [] unless backtrace
return backtrace if options[:full_backtrace] || backtrace.empty?
backtrace.map { |l| backtrace_line(l) }.compact.inject([]) do |result, line|
break result if line.include?(SCRIPT_NAME)
result << line
end
end
end
entry_point
#!/bin/bash
set -eu
cd $(dirname $0)/..
NETCAT="nc -N" # 環境に応じて調整
raw_output=$(echo "$@" | $NETCAT 127.0.0.1 3002)
status=$(echo "$raw_output" | head -n 1)
echo "$raw_output" | tail -n +2
exit $status
rubocop-daemonのアイデアが素晴らしいので、同じノリでrspecもできないか?という試み。
rspecが速いとTDDが捗りそう。
想定
- rspec
- rails
- daemonを起動しておき、コマンドをTCPで受けて実行する
- クライアントからはnc使う
rubocop-daemonと同じようにするならrails前提にする必要は無いのだが、railsだとrails runner
でカジュアルにスクリプトを突っ込めて導入しやすいのではないかというところ
実行すべき関数を探る。rspecの場合はだいたいbundle exec rspec
的な感じで実行するので、rspec
コマンドから辿る。
#!/usr/bin/env ruby
require 'rspec/core'
RSpec::Core::Runner.invoke
引数とかそういうのが見当たらないのでRSpec::Core::Runner.invoke
を見にいく。
def self.invoke
disable_autorun!
status = run(ARGV, $stderr, $stdout).to_i
exit(status) if status != 0
end
disable_autorun!
はフラグをセットしてるだけなので、runを実行すればいいっぽい。めちゃくちゃCLIの入出力をそのまま指定してるし。
rspec
rspec-rails
の入ったrailsプロジェクトにてbundle exec rails runner 'RSpec::Core::Runner.run([], $stderr, $stdout)'
としてみると、いつもの結果出力が見える。ok。
であれば、あとはTCPサーバ立てて入出力をあれこれしてこれに渡せばよいっぽい。
とりあえずecho serverをこしらえる。rubocop-daemonを参考にしつつ。
# frozen_string_literal: true
require 'socket'
def read_socket(socket)
msg = socket.read
puts msg
socket.write(msg)
rescue StandardError => e
socket.puts e.full_message
ensure
socket.close
end
server = TCPServer.open('0.0.0.0', 3002)
loop do
read_socket(server.accept)
rescue Interrupt
puts 'quit'
server.close
break
end
$ ruby lib/rspec_daemon.rb
aaa
$ echo aaa | nc -N 127.0.0.1 3002
aaa
※筆者はWSLなのでnc -N
です
なんか動くやつ
# frozen_string_literal: true
require 'socket'
require 'stringio'
def run(msg)
argv = msg.split(' ')
err = StringIO.new
out = StringIO.new
status = RSpec::Core::Runner.run(argv, err, out)
[status, err, out]
end
def read_socket(socket)
status, err, out = run(socket.read)
socket.puts(status)
socket.puts(out.string)
socket.puts(err.string)
...
ただし起動後の最初の一回しかテストが実行されない。なにか実行済フラグ的なのがあるのかもしれない。
なんか似た何かをしているプロジェクト
コードを追うと、RSpec.reset
が必要らしい。これをRSpec::Core::Runner.run
の前に仕込んだら毎回テストしてくれるようになった。
コマンドステータスなどはともかく、とにかく動くα版
# frozen_string_literal: true
require 'socket'
require 'stringio'
def run(msg)
argv = msg.split(' ')
RSpec.reset
out = StringIO.new
status = RSpec::Core::Runner.run(argv, out, out)
[status, out.string]
end
def read_socket(socket)
status, out = run(socket.read)
socket.puts(status)
socket.puts(out)
rescue StandardError => e
socket.puts e.full_message
ensure
socket.close
end
RSpec::Core::Runner.disable_autorun!
server = TCPServer.open('0.0.0.0', 3002)
loop do
read_socket(server.accept)
rescue Interrupt
puts 'quit'
server.close
break
end
errとoutを分けると順番がおかしくなることに気付いたのでマージ。
あとテスト失敗時のエラー出力が出過ぎなので調整
- 失敗時のスタックトレースを抑制する(daemonスクリプトの行まで全部出ちゃってる)
- 出力の色が死んでるので取り出す
スタックトレース、rspec自体の機能としてフィルタリングする機能があるのだが、この場合は「rspec_daemon以降(以前?)のトレースを全部消す」なので、これではrunnerとかspringとかbootsnapとかそのへんがいい感じに消せない。
そこで秘奥義class_eval先生にご登場願う。
RSpec::Core::BacktraceFormatter.class_eval do
def format_backtrace(backtrace, options = {})
return [] unless backtrace
return backtrace if options[:full_backtrace] || backtrace.empty?
backtrace.map { |l| backtrace_line(l) }.compact.inject([]) do |result, line|
break result if line =~ /rspec_daemon\.rb/
result << line
end
end
end
※トレースが空の場合のヘルプメッセージは長いので割愛
これでいい感じになる。
出力の色は--force-color
オプションつければいいだけだった。
def run(msg, options = [])
options += ['--force-color']
argv = msg.split(' ')
RSpec.reset
out = StringIO.new
status = RSpec::Core::Runner.run(options + argv, out, out)
[status, out.string]
end
クライアントスクリプト
#!/bin/bash
set -eu
cd $(dirname $0)/..
NETCAT="nc -N" # 環境に応じて調整
raw_output=$(echo "$@" | $NETCAT 127.0.0.1 3002)
status=$(echo "$raw_output" | head -n 1)
echo "$raw_output" | tail -n +2
exit $status