|
|
#!/usr/bin/env bash
|
||
|
|
#
|
||
|
|
# RTK Smoke Tests — Ruby (RSpec, RuboCop, Minitest, Bundle)
|
||
|
|
# Creates a minimal Rails app, exercises all Ruby RTK filters, then cleans up.
|
||
|
|
# Usage: bash scripts/test-ruby.sh
|
||
|
|
#
|
||
|
|
# Prerequisites: rtk (installed), ruby, bundler, rails gem
|
||
|
|
# Duration: ~60-120s (rails new + bundle install dominate)
|
||
|
|
#
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
PASS=0
|
||
|
|
FAIL=0
|
||
|
|
SKIP=0
|
||
|
|
FAILURES=()
|
||
|
|
|
||
|
|
RED='\033[0;31m'
|
||
|
|
GREEN='\033[0;32m'
|
||
|
|
YELLOW='\033[0;33m'
|
||
|
|
CYAN='\033[0;36m'
|
||
|
|
BOLD='\033[1m'
|
||
|
|
NC='\033[0m'
|
||
|
|
|
||
|
|
# ── Helpers ──────────────────────────────────────────
|
||
|
|
|
||
|
|
assert_ok() {
|
||
|
|
local name="$1"; shift
|
||
|
|
local output
|
||
|
|
if output=$("$@" 2>&1); then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} %s\n" "$name"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("$name")
|
||
|
|
printf " ${RED}FAIL${NC} %s\n" "$name"
|
||
|
|
printf " cmd: %s\n" "$*"
|
||
|
|
printf " out: %s\n" "$(echo "$output" | head -3)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
assert_contains() {
|
||
|
|
local name="$1"; local needle="$2"; shift 2
|
||
|
|
local output
|
||
|
|
if output=$("$@" 2>&1) && echo "$output" | grep -q "$needle"; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} %s\n" "$name"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("$name")
|
||
|
|
printf " ${RED}FAIL${NC} %s\n" "$name"
|
||
|
|
printf " expected: '%s'\n" "$needle"
|
||
|
|
printf " got: %s\n" "$(echo "$output" | head -3)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# Allow non-zero exit but check output
|
||
|
|
assert_output() {
|
||
|
|
local name="$1"; local needle="$2"; shift 2
|
||
|
|
local output
|
||
|
|
output=$("$@" 2>&1) || true
|
||
|
|
if echo "$output" | grep -qi "$needle"; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} %s\n" "$name"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("$name")
|
||
|
|
printf " ${RED}FAIL${NC} %s\n" "$name"
|
||
|
|
printf " expected: '%s'\n" "$needle"
|
||
|
|
printf " got: %s\n" "$(echo "$output" | head -3)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
skip_test() {
|
||
|
|
local name="$1"; local reason="$2"
|
||
|
|
SKIP=$((SKIP + 1))
|
||
|
|
printf " ${YELLOW}SKIP${NC} %s (%s)\n" "$name" "$reason"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Assert command exits with non-zero and output matches needle
|
||
|
|
assert_exit_nonzero() {
|
||
|
|
local name="$1"; local needle="$2"; shift 2
|
||
|
|
local output
|
||
|
|
local rc=0
|
||
|
|
output=$("$@" 2>&1) || rc=$?
|
||
|
|
if [[ $rc -ne 0 ]] && echo "$output" | grep -qi "$needle"; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} %s (exit=%d)\n" "$name" "$rc"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("$name")
|
||
|
|
printf " ${RED}FAIL${NC} %s (exit=%d)\n" "$name" "$rc"
|
||
|
|
if [[ $rc -eq 0 ]]; then
|
||
|
|
printf " expected non-zero exit, got 0\n"
|
||
|
|
else
|
||
|
|
printf " expected: '%s'\n" "$needle"
|
||
|
|
fi
|
||
|
|
printf " out: %s\n" "$(echo "$output" | head -3)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
section() {
|
||
|
|
printf "\n${BOLD}${CYAN}── %s ──${NC}\n" "$1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Prerequisite checks ─────────────────────────────
|
||
|
|
|
||
|
|
RTK=$(command -v rtk || echo "")
|
||
|
|
if [[ -z "$RTK" ]]; then
|
||
|
|
echo "rtk not found in PATH. Run: cargo install --path ."
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
if ! command -v ruby >/dev/null 2>&1; then
|
||
|
|
echo "ruby not found in PATH. Install Ruby first."
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
if ! command -v bundle >/dev/null 2>&1; then
|
||
|
|
echo "bundler not found in PATH. Run: gem install bundler"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
if ! command -v rails >/dev/null 2>&1; then
|
||
|
|
echo "rails not found in PATH. Run: gem install rails"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ── Preamble ─────────────────────────────────────────
|
||
|
|
|
||
|
|
printf "${BOLD}RTK Smoke Tests — Ruby (RSpec, RuboCop, Minitest, Bundle)${NC}\n"
|
||
|
|
printf "Binary: %s (%s)\n" "$RTK" "$(rtk --version)"
|
||
|
|
printf "Ruby: %s\n" "$(ruby --version)"
|
||
|
|
printf "Rails: %s\n" "$(rails --version)"
|
||
|
|
printf "Bundler: %s\n" "$(bundle --version)"
|
||
|
|
printf "Date: %s\n\n" "$(date '+%Y-%m-%d %H:%M')"
|
||
|
|
|
||
|
|
# ── Temp dir + cleanup trap ──────────────────────────
|
||
|
|
|
||
|
|
TMPDIR=$(mktemp -d /tmp/rtk-ruby-smoke-XXXXXX)
|
||
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||
|
|
|
||
|
|
printf "${BOLD}Setting up temporary Rails app in %s ...${NC}\n" "$TMPDIR"
|
||
|
|
|
||
|
|
# ── Setup phase (not counted in assertions) ──────────
|
||
|
|
|
||
|
|
cd "$TMPDIR"
|
||
|
|
|
||
|
|
# 1. Create minimal Rails app
|
||
|
|
printf " → rails new (--minimal --skip-git --skip-docker) ...\n"
|
||
|
|
rails new rtk_smoke_app --minimal --skip-git --skip-docker --quiet 2>&1 | tail -1 || true
|
||
|
|
cd rtk_smoke_app
|
||
|
|
|
||
|
|
# 2. Add rspec-rails and rubocop to Gemfile
|
||
|
|
cat >> Gemfile <<'GEMFILE'
|
||
|
|
|
||
|
|
group :development, :test do
|
||
|
|
gem 'rspec-rails'
|
||
|
|
gem 'rubocop', require: false
|
||
|
|
end
|
||
|
|
GEMFILE
|
||
|
|
|
||
|
|
# 3. Bundle install
|
||
|
|
printf " → bundle install ...\n"
|
||
|
|
bundle install --quiet 2>&1 | tail -1 || true
|
||
|
|
|
||
|
|
# 4. Generate scaffold (creates model + minitest files)
|
||
|
|
printf " → rails generate scaffold Post ...\n"
|
||
|
|
rails generate scaffold Post title:string body:text published:boolean --quiet 2>&1 | tail -1 || true
|
||
|
|
|
||
|
|
# 5. Install RSpec + create manual spec file
|
||
|
|
printf " → rails generate rspec:install ...\n"
|
||
|
|
rails generate rspec:install --quiet 2>&1 | tail -1 || true
|
||
|
|
|
||
|
|
mkdir -p spec/models
|
||
|
|
cat > spec/models/post_spec.rb <<'SPEC'
|
||
|
|
require 'rails_helper'
|
||
|
|
|
||
|
|
RSpec.describe Post, type: :model do
|
||
|
|
it "is valid with valid attributes" do
|
||
|
|
post = Post.new(title: "Test", body: "Body", published: false)
|
||
|
|
expect(post).to be_valid
|
||
|
|
end
|
||
|
|
end
|
||
|
|
SPEC
|
||
|
|
|
||
|
|
# 6. Create + migrate database
|
||
|
|
printf " → rails db:create && db:migrate ...\n"
|
||
|
|
rails db:create --quiet 2>&1 | tail -1 || true
|
||
|
|
rails db:migrate --quiet 2>&1 | tail -1 || true
|
||
|
|
|
||
|
|
# 7. Create a file with intentional RuboCop offenses
|
||
|
|
printf " → creating rubocop_bait.rb with intentional offenses ...\n"
|
||
|
|
cat > app/models/rubocop_bait.rb <<'BAIT'
|
||
|
|
class RubocopBait < ApplicationRecord
|
||
|
|
def messy_method()
|
||
|
|
x = 1
|
||
|
|
y = 2
|
||
|
|
if x == 1
|
||
|
|
puts "hello world"
|
||
|
|
end
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
BAIT
|
||
|
|
|
||
|
|
# 8. Create a failing RSpec spec
|
||
|
|
printf " → creating failing rspec spec ...\n"
|
||
|
|
cat > spec/models/post_fail_spec.rb <<'FAILSPEC'
|
||
|
|
require 'rails_helper'
|
||
|
|
|
||
|
|
RSpec.describe Post, type: :model do
|
||
|
|
it "intentionally fails validation check" do
|
||
|
|
post = Post.new(title: "Hello", body: "World", published: false)
|
||
|
|
expect(post.title).to eq("Wrong Title On Purpose")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
FAILSPEC
|
||
|
|
|
||
|
|
# 9. Create an RSpec spec with pending example
|
||
|
|
printf " → creating rspec spec with pending example ...\n"
|
||
|
|
cat > spec/models/post_pending_spec.rb <<'PENDSPEC'
|
||
|
|
require 'rails_helper'
|
||
|
|
|
||
|
|
RSpec.describe Post, type: :model do
|
||
|
|
it "is valid with title" do
|
||
|
|
post = Post.new(title: "OK", body: "Body", published: false)
|
||
|
|
expect(post).to be_valid
|
||
|
|
end
|
||
|
|
|
||
|
|
it "will support markdown later" do
|
||
|
|
pending "Not yet implemented"
|
||
|
|
expect(Post.new.render_markdown).to eq("<p>hello</p>")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
PENDSPEC
|
||
|
|
|
||
|
|
# 10. Create a failing minitest test
|
||
|
|
printf " → creating failing minitest test ...\n"
|
||
|
|
cat > test/models/post_fail_test.rb <<'FAILTEST'
|
||
|
|
require "test_helper"
|
||
|
|
|
||
|
|
class PostFailTest < ActiveSupport::TestCase
|
||
|
|
test "intentionally fails" do
|
||
|
|
assert_equal "wrong", Post.new(title: "right").title
|
||
|
|
end
|
||
|
|
end
|
||
|
|
FAILTEST
|
||
|
|
|
||
|
|
# 11. Create a passing minitest test
|
||
|
|
printf " → creating passing minitest test ...\n"
|
||
|
|
cat > test/models/post_pass_test.rb <<'PASSTEST'
|
||
|
|
require "test_helper"
|
||
|
|
|
||
|
|
class PostPassTest < ActiveSupport::TestCase
|
||
|
|
test "post is valid" do
|
||
|
|
post = Post.new(title: "OK", body: "Body", published: false)
|
||
|
|
assert post.valid?
|
||
|
|
end
|
||
|
|
end
|
||
|
|
PASSTEST
|
||
|
|
|
||
|
|
printf "\n${BOLD}Setup complete. Running tests...${NC}\n"
|
||
|
|
|
||
|
|
# ══════════════════════════════════════════════════════
|
||
|
|
# Test sections
|
||
|
|
# ══════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
# ── 1. RSpec ─────────────────────────────────────────
|
||
|
|
|
||
|
|
section "RSpec"
|
||
|
|
|
||
|
|
assert_output "rtk rspec (with failure)" \
|
||
|
|
"failed" \
|
||
|
|
rtk rspec
|
||
|
|
|
||
|
|
assert_output "rtk rspec spec/models/post_spec.rb (pass)" \
|
||
|
|
"RSpec.*passed" \
|
||
|
|
rtk rspec spec/models/post_spec.rb
|
||
|
|
|
||
|
|
assert_output "rtk rspec spec/models/post_fail_spec.rb (fail)" \
|
||
|
|
"failed\|❌" \
|
||
|
|
rtk rspec spec/models/post_fail_spec.rb
|
||
|
|
|
||
|
|
# ── 2. RuboCop ───────────────────────────────────────
|
||
|
|
|
||
|
|
section "RuboCop"
|
||
|
|
|
||
|
|
assert_output "rtk rubocop (with offenses)" \
|
||
|
|
"offense" \
|
||
|
|
rtk rubocop
|
||
|
|
|
||
|
|
assert_output "rtk rubocop app/ (with offenses)" \
|
||
|
|
"rubocop_bait\|offense" \
|
||
|
|
rtk rubocop app/
|
||
|
|
|
||
|
|
# ── 3. Minitest (rake test) ──────────────────────────
|
||
|
|
|
||
|
|
section "Minitest (rake test)"
|
||
|
|
|
||
|
|
assert_output "rtk rake test (with failure)" \
|
||
|
|
"failure\|error\|FAIL" \
|
||
|
|
rtk rake test
|
||
|
|
|
||
|
|
assert_output "rtk rake test single passing file" \
|
||
|
|
"ok rake test\|0 failures" \
|
||
|
|
rtk rake test TEST=test/models/post_pass_test.rb
|
||
|
|
|
||
|
|
assert_exit_nonzero "rtk rake test single failing file" \
|
||
|
|
"failure\|FAIL" \
|
||
|
|
rtk rake test test/models/post_fail_test.rb
|
||
|
|
|
||
|
|
# ── 4. Bundle install ────────────────────────────────
|
||
|
|
|
||
|
|
section "Bundle install"
|
||
|
|
|
||
|
|
assert_output "rtk bundle install (idempotent)" \
|
||
|
|
"bundle\|ok\|complete\|install" \
|
||
|
|
rtk bundle install
|
||
|
|
|
||
|
|
# ── 5. Exit code preservation ────────────────────────
|
||
|
|
|
||
|
|
section "Exit code preservation"
|
||
|
|
|
||
|
|
assert_exit_nonzero "rtk rspec exits non-zero on failure" \
|
||
|
|
"failed\|failure" \
|
||
|
|
rtk rspec spec/models/post_fail_spec.rb
|
||
|
|
|
||
|
|
assert_exit_nonzero "rtk rubocop exits non-zero on offenses" \
|
||
|
|
"offense" \
|
||
|
|
rtk rubocop app/models/rubocop_bait.rb
|
||
|
|
|
||
|
|
assert_exit_nonzero "rtk rake test exits non-zero on failure" \
|
||
|
|
"failure\|FAIL" \
|
||
|
|
rtk rake test test/models/post_fail_test.rb
|
||
|
|
|
||
|
|
# ── 6. bundle exec variants ─────────────────────────
|
||
|
|
|
||
|
|
section "bundle exec variants"
|
||
|
|
|
||
|
|
assert_output "bundle exec rspec spec/models/post_spec.rb" \
|
||
|
|
"passed\|example" \
|
||
|
|
rtk bundle exec rspec spec/models/post_spec.rb
|
||
|
|
|
||
|
|
assert_output "bundle exec rubocop app/" \
|
||
|
|
"offense" \
|
||
|
|
rtk bundle exec rubocop app/
|
||
|
|
|
||
|
|
# ── 7. RuboCop autocorrect ───────────────────────────
|
||
|
|
|
||
|
|
section "RuboCop autocorrect"
|
||
|
|
|
||
|
|
# Copy bait file so autocorrect has something to fix
|
||
|
|
cp app/models/rubocop_bait.rb app/models/rubocop_bait_ac.rb
|
||
|
|
sed -i.bak 's/RubocopBait/RubocopBaitAc/' app/models/rubocop_bait_ac.rb
|
||
|
|
|
||
|
|
assert_output "rtk rubocop -A (autocorrect)" \
|
||
|
|
"autocorrected\|rubocop\|ok\|offense\|inspected" \
|
||
|
|
rtk rubocop -A app/models/rubocop_bait_ac.rb
|
||
|
|
|
||
|
|
# Clean up autocorrect test file
|
||
|
|
rm -f app/models/rubocop_bait_ac.rb app/models/rubocop_bait_ac.rb.bak
|
||
|
|
|
||
|
|
# ── 8. RSpec pending ─────────────────────────────────
|
||
|
|
|
||
|
|
section "RSpec pending"
|
||
|
|
|
||
|
|
assert_output "rtk rspec with pending example" \
|
||
|
|
"pending" \
|
||
|
|
rtk rspec spec/models/post_pending_spec.rb
|
||
|
|
|
||
|
|
# ── 9. RSpec text fallback ───────────────────────────
|
||
|
|
|
||
|
|
section "RSpec text fallback"
|
||
|
|
|
||
|
|
assert_output "rtk rspec --format documentation (text path)" \
|
||
|
|
"valid\|example\|post" \
|
||
|
|
rtk rspec --format documentation spec/models/post_spec.rb
|
||
|
|
|
||
|
|
# ── 10. RSpec empty suite ────────────────────────────
|
||
|
|
|
||
|
|
section "RSpec empty suite"
|
||
|
|
|
||
|
|
assert_output "rtk rspec nonexistent tag" \
|
||
|
|
"0 examples\|No examples" \
|
||
|
|
rtk rspec --tag nonexistent spec/models/post_spec.rb
|
||
|
|
|
||
|
|
# ── 11. Token savings ────────────────────────────────
|
||
|
|
|
||
|
|
section "Token savings"
|
||
|
|
|
||
|
|
# rspec (passing spec)
|
||
|
|
raw_len=$( (bundle exec rspec spec/models/post_spec.rb 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
rtk_len=$( (rtk rspec spec/models/post_spec.rb 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
if [[ "$rtk_len" -lt "$raw_len" ]]; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} rspec: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("token savings: rspec")
|
||
|
|
printf " ${RED}FAIL${NC} rspec: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# rubocop (exits non-zero on offenses, so || true)
|
||
|
|
raw_len=$( (bundle exec rubocop app/ 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
rtk_len=$( (rtk rubocop app/ 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
if [[ "$rtk_len" -lt "$raw_len" ]]; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} rubocop: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("token savings: rubocop")
|
||
|
|
printf " ${RED}FAIL${NC} rubocop: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# rake test (passing file)
|
||
|
|
raw_len=$( (bundle exec rake test TEST=test/models/post_pass_test.rb 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
rtk_len=$( (rtk rake test test/models/post_pass_test.rb 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
if [[ "$rtk_len" -lt "$raw_len" ]]; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} rake test: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("token savings: rake test")
|
||
|
|
printf " ${RED}FAIL${NC} rake test: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# bundle install (idempotent)
|
||
|
|
raw_len=$( (bundle install 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
rtk_len=$( (rtk bundle install 2>&1 || true) | wc -c | tr -d ' ')
|
||
|
|
if [[ "$rtk_len" -lt "$raw_len" ]]; then
|
||
|
|
PASS=$((PASS + 1))
|
||
|
|
printf " ${GREEN}PASS${NC} bundle install: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
else
|
||
|
|
FAIL=$((FAIL + 1))
|
||
|
|
FAILURES+=("token savings: bundle install")
|
||
|
|
printf " ${RED}FAIL${NC} bundle install: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ── 12. Verbose flag ─────────────────────────────────
|
||
|
|
|
||
|
|
section "Verbose flag (-v)"
|
||
|
|
|
||
|
|
assert_output "rtk -v rspec (verbose)" \
|
||
|
|
"RSpec\|passed\|Running\|example" \
|
||
|
|
rtk -v rspec spec/models/post_spec.rb
|
||
|
|
|
||
|
|
# ══════════════════════════════════════════════════════
|
||
|
|
# Report
|
||
|
|
# ══════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
printf "\n${BOLD}══════════════════════════════════════${NC}\n"
|
||
|
|
printf "${BOLD}Results: ${GREEN}%d passed${NC}, ${RED}%d failed${NC}, ${YELLOW}%d skipped${NC}\n" "$PASS" "$FAIL" "$SKIP"
|
||
|
|
|
||
|
|
if [[ ${#FAILURES[@]} -gt 0 ]]; then
|
||
|
|
printf "\n${RED}Failures:${NC}\n"
|
||
|
|
for f in "${FAILURES[@]}"; do
|
||
|
|
printf " - %s\n" "$f"
|
||
|
|
done
|
||
|
|
fi
|
||
|
|
|
||
|
|
printf "${BOLD}══════════════════════════════════════${NC}\n"
|
||
|
|
|
||
|
|
exit "$FAIL"
|