Skip to content

Commit 26708cc

Browse files
etewiahclaude
andcommitted
Add smoke test script for pre-deployment validation
Provides quick sanity checks to catch configuration and loading issues before deployment. Checks include: - Ruby version and bundler status - Rails environment loading - Eager loading (catches autoload path issues like the R2 service) - ActiveStorage R2 service availability - Cache store connectivity - Database connection and migrations - Theme loading - Asset pipeline Usage: bin/smoke_test # Full test bin/smoke_test --quick # Skip database checks bin/smoke_test --verbose # Show timing and details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c2c08ba commit 26708cc

File tree

1 file changed

+266
-0
lines changed

1 file changed

+266
-0
lines changed

bin/smoke_test

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Smoke Test Script for PropertyWebBuilder
5+
#
6+
# Runs quick sanity checks to verify the application can start correctly
7+
# and critical services are available. Run this before deploying to catch
8+
# configuration and loading issues early.
9+
#
10+
# Usage:
11+
# bin/smoke_test # Run all checks
12+
# bin/smoke_test --quick # Skip slow checks (database queries)
13+
# bin/smoke_test --verbose # Show detailed output
14+
#
15+
# Exit codes:
16+
# 0 - All checks passed
17+
# 1 - One or more checks failed
18+
19+
require "optparse"
20+
21+
options = { verbose: false, quick: false }
22+
OptionParser.new do |opts|
23+
opts.banner = "Usage: bin/smoke_test [options]"
24+
opts.on("-v", "--verbose", "Show detailed output") { options[:verbose] = true }
25+
opts.on("-q", "--quick", "Skip slow checks") { options[:quick] = true }
26+
opts.on("-h", "--help", "Show this help") { puts opts; exit }
27+
end.parse!
28+
29+
# ANSI color codes
30+
class Colors
31+
def self.green(text) = "\e[32m#{text}\e[0m"
32+
def self.red(text) = "\e[31m#{text}\e[0m"
33+
def self.yellow(text) = "\e[33m#{text}\e[0m"
34+
def self.blue(text) = "\e[34m#{text}\e[0m"
35+
def self.bold(text) = "\e[1m#{text}\e[0m"
36+
end
37+
38+
class SmokeTest
39+
attr_reader :options, :results
40+
41+
def initialize(options)
42+
@options = options
43+
@results = { passed: [], failed: [], skipped: [] }
44+
end
45+
46+
def run
47+
puts Colors.bold("\nPropertyWebBuilder Smoke Test")
48+
puts "=" * 40
49+
puts
50+
51+
# Phase 1: Pre-Rails checks
52+
section("Environment") do
53+
check("Ruby version") { ruby_version_check }
54+
check("Bundler") { bundler_check }
55+
end
56+
57+
# Phase 2: Rails loading
58+
section("Rails Application") do
59+
check("Environment loads") { rails_environment_check }
60+
end
61+
62+
# Load Rails for remaining checks
63+
load_rails!
64+
65+
# Phase 2b: Post-load Rails checks
66+
section("Rails Loading") do
67+
check("Eager loading") { eager_load_check }
68+
end
69+
70+
# Phase 3: Service checks
71+
section("Services") do
72+
check("ActiveStorage R2 service") { r2_service_check }
73+
check("Cache store") { cache_check }
74+
end
75+
76+
# Phase 4: Database checks (skip with --quick)
77+
section("Database", skip: options[:quick]) do
78+
check("Database connection") { database_connection_check }
79+
check("Migrations current") { migrations_check }
80+
check("Website model") { website_model_check }
81+
check("Theme loading") { theme_loading_check }
82+
end
83+
84+
# Phase 5: Asset checks
85+
section("Assets") do
86+
check("Asset pipeline") { asset_pipeline_check }
87+
end
88+
89+
print_summary
90+
exit(@results[:failed].empty? ? 0 : 1)
91+
end
92+
93+
private
94+
95+
def section(name, skip: false)
96+
puts Colors.blue("#{name}:")
97+
if skip
98+
puts " #{Colors.yellow('SKIPPED')} (use without --quick to run)"
99+
puts
100+
return
101+
end
102+
yield
103+
puts
104+
end
105+
106+
def check(name)
107+
print " #{name}... "
108+
$stdout.flush
109+
110+
start_time = Time.now
111+
result = yield
112+
elapsed = ((Time.now - start_time) * 1000).round
113+
114+
if result[:success]
115+
@results[:passed] << name
116+
puts Colors.green("OK") + (options[:verbose] ? " (#{elapsed}ms)" : "")
117+
puts " #{result[:detail]}" if options[:verbose] && result[:detail]
118+
else
119+
@results[:failed] << { name: name, error: result[:error] }
120+
puts Colors.red("FAILED")
121+
puts " #{Colors.red(result[:error])}"
122+
end
123+
rescue StandardError => e
124+
@results[:failed] << { name: name, error: e.message }
125+
puts Colors.red("FAILED")
126+
puts " #{Colors.red(e.message)}"
127+
puts " #{e.backtrace.first(3).join("\n ")}" if options[:verbose]
128+
end
129+
130+
def ruby_version_check
131+
required = File.read(".ruby-version").strip rescue nil
132+
current = RUBY_VERSION
133+
134+
if required && current != required
135+
{ success: false, error: "Expected #{required}, got #{current}" }
136+
else
137+
{ success: true, detail: "Ruby #{current}" }
138+
end
139+
end
140+
141+
def bundler_check
142+
output = `bundle check 2>&1`
143+
if $?.success?
144+
{ success: true, detail: "All gems installed" }
145+
else
146+
{ success: false, error: output.lines.first&.strip || "Bundle check failed" }
147+
end
148+
end
149+
150+
def rails_environment_check
151+
# Verify config files exist
152+
%w[config/application.rb config/environment.rb].each do |file|
153+
unless File.exist?(file)
154+
return { success: false, error: "Missing #{file}" }
155+
end
156+
end
157+
{ success: true, detail: "Config files present" }
158+
end
159+
160+
def eager_load_check
161+
# Skip if already eager loaded (production mode)
162+
return { success: true, detail: "Already eager loaded" } if Rails.application.config.eager_load
163+
164+
Rails.application.eager_load!
165+
{ success: true, detail: "All application code loaded" }
166+
end
167+
168+
def load_rails!
169+
puts " Loading Rails environment..."
170+
$stdout.flush
171+
start = Time.now
172+
require_relative "../config/environment"
173+
elapsed = ((Time.now - start) * 1000).round
174+
puts " #{Colors.green('OK')} (#{elapsed}ms)"
175+
puts
176+
rescue StandardError => e
177+
puts Colors.red("\nFATAL: Could not load Rails environment")
178+
puts e.message
179+
puts e.backtrace.first(5).join("\n") if options[:verbose]
180+
exit 1
181+
end
182+
183+
def r2_service_check
184+
# Verify the R2 service class is loadable
185+
klass = ActiveStorage::Service::R2Service
186+
{ success: true, detail: klass.to_s }
187+
rescue NameError, LoadError => e
188+
{ success: false, error: e.message }
189+
end
190+
191+
def cache_check
192+
test_key = "smoke_test_#{Time.now.to_i}"
193+
Rails.cache.write(test_key, "test_value", expires_in: 1.minute)
194+
value = Rails.cache.read(test_key)
195+
Rails.cache.delete(test_key)
196+
197+
if value == "test_value"
198+
{ success: true, detail: Rails.cache.class.name }
199+
else
200+
{ success: false, error: "Cache read/write failed" }
201+
end
202+
end
203+
204+
def database_connection_check
205+
ActiveRecord::Base.connection.execute("SELECT 1")
206+
adapter = ActiveRecord::Base.connection.adapter_name
207+
{ success: true, detail: adapter }
208+
end
209+
210+
def migrations_check
211+
context = ActiveRecord::MigrationContext.new(Rails.root.join("db/migrate"))
212+
pending = context.migrations.size - context.get_all_versions.size
213+
214+
if pending.zero?
215+
{ success: true, detail: "All migrations applied" }
216+
else
217+
{ success: false, error: "#{pending} pending migration(s)" }
218+
end
219+
end
220+
221+
def website_model_check
222+
count = Pwb::Website.count
223+
{ success: true, detail: "#{count} website(s) in database" }
224+
end
225+
226+
def theme_loading_check
227+
themes = Pwb::Theme.all.map(&:name)
228+
if themes.any?
229+
{ success: true, detail: "Themes: #{themes.join(', ')}" }
230+
else
231+
{ success: false, error: "No themes found" }
232+
end
233+
end
234+
235+
def asset_pipeline_check
236+
# Check that Propshaft/Sprockets is configured
237+
if defined?(Propshaft) || defined?(Sprockets)
238+
pipeline = defined?(Propshaft) ? "Propshaft" : "Sprockets"
239+
{ success: true, detail: pipeline }
240+
else
241+
{ success: false, error: "No asset pipeline configured" }
242+
end
243+
end
244+
245+
def print_summary
246+
puts "=" * 40
247+
puts Colors.bold("Summary")
248+
puts
249+
250+
total = @results[:passed].size + @results[:failed].size
251+
passed = @results[:passed].size
252+
failed = @results[:failed].size
253+
254+
if failed.zero?
255+
puts Colors.green("All #{total} checks passed!")
256+
else
257+
puts Colors.red("#{failed} of #{total} checks failed:")
258+
@results[:failed].each do |failure|
259+
puts " - #{failure[:name]}: #{failure[:error]}"
260+
end
261+
end
262+
puts
263+
end
264+
end
265+
266+
SmokeTest.new(options).run

0 commit comments

Comments
 (0)