railstest 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/bin/railstest +6 -0
- data/docker-compose.yml +18 -0
- data/lib/railstest/cli.rb +477 -0
- data/lib/railstest/database_manager.rb +117 -0
- data/lib/railstest/docker_manager.rb +291 -0
- data/lib/railstest/supported_versions.rb +74 -0
- data/lib/railstest/test_runner.rb +236 -0
- data/lib/railstest/version.rb +5 -0
- data/lib/railstest.rb +12 -0
- metadata +57 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Railstest
|
|
7
|
+
class DockerManager
|
|
8
|
+
attr_reader :ruby_version, :rails_version, :gem_path, :database
|
|
9
|
+
|
|
10
|
+
def initialize(ruby_version:, rails_version:, gem_path: nil, database: nil)
|
|
11
|
+
@ruby_version = ruby_version
|
|
12
|
+
@rails_version = rails_version
|
|
13
|
+
@gem_path = gem_path
|
|
14
|
+
@database = database
|
|
15
|
+
@target_gem_name = nil
|
|
16
|
+
validate_docker!
|
|
17
|
+
validate_gem_path! if target_gem_mode?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_image
|
|
21
|
+
puts "Building Docker image for Ruby #{ruby_version} and Rails #{rails_version}..."
|
|
22
|
+
dockerfile_path = File.join(Dir.tmpdir, "railstest_dockerfile_#{Process.pid}")
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
File.write(dockerfile_path, generate_dockerfile)
|
|
26
|
+
|
|
27
|
+
build_args = [
|
|
28
|
+
'--build-arg', "RUBY_VERSION=#{ruby_version}"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
if target_gem_mode?
|
|
32
|
+
build_args << '--build-arg' << "TARGET_GEM_NAME=#{target_gem_name}"
|
|
33
|
+
build_args << '--build-arg' << "RAILS_VERSION=#{rails_version}"
|
|
34
|
+
else
|
|
35
|
+
# Local mode: find actual gemfile and pass it
|
|
36
|
+
gemfile = find_gemfile_for_version
|
|
37
|
+
build_args << '--build-arg' << "GEMFILE_PATH=gemfiles/#{gemfile}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
success = system(
|
|
41
|
+
'docker', 'build', '.',
|
|
42
|
+
'-f', dockerfile_path,
|
|
43
|
+
*build_args,
|
|
44
|
+
'-t', image_name
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
raise Error, 'Docker build failed' unless success
|
|
48
|
+
ensure
|
|
49
|
+
FileUtils.rm_f(dockerfile_path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def run_command(command, env_vars: {}, volumes: [], workdir: nil)
|
|
54
|
+
docker_cmd = ['docker', 'run', '--rm', '--network=host']
|
|
55
|
+
|
|
56
|
+
env_vars.each do |key, value|
|
|
57
|
+
docker_cmd << '-e' << "#{key}=#{value}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
volumes.each do |volume|
|
|
61
|
+
docker_cmd << '-v' << volume
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
docker_cmd << '-w' << workdir if workdir
|
|
65
|
+
|
|
66
|
+
docker_cmd << image_name
|
|
67
|
+
docker_cmd.concat(Array(command))
|
|
68
|
+
|
|
69
|
+
system(*docker_cmd)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def image_name
|
|
73
|
+
@image_name ||= begin
|
|
74
|
+
base = File.basename(Dir.pwd).downcase.gsub(/[^a-z0-9._-]/, '-')
|
|
75
|
+
ruby_tag = ruby_version.to_s.gsub(/[^a-z0-9._-]/, '-')
|
|
76
|
+
rails_tag = rails_version.to_s.gsub(/[^a-z0-9._-]/, '-')
|
|
77
|
+
"#{base}-ruby#{ruby_tag}-rails#{rails_tag}-tests"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def target_gem_mode?
|
|
82
|
+
!gem_path.nil? && !gem_path.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def target_gem_name
|
|
86
|
+
return @target_gem_name if @target_gem_name
|
|
87
|
+
return nil unless target_gem_mode?
|
|
88
|
+
|
|
89
|
+
@target_gem_name = extract_gem_name(gem_path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def expanded_gem_path
|
|
93
|
+
return nil unless target_gem_mode?
|
|
94
|
+
|
|
95
|
+
File.expand_path(gem_path)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rails_version_for_gemfile
|
|
99
|
+
# Convert dotted format to underscore for gemfile paths (e.g., 7.1 -> 7_1)
|
|
100
|
+
rails_version.tr('.', '_')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_gemfile_for_version
|
|
104
|
+
# Find actual gemfile in gemfiles/ directory that matches the Rails version
|
|
105
|
+
# Handles various naming conventions: rails_7.0.gemfile, Gemfile.rails-5.2-rc1, etc.
|
|
106
|
+
return nil if target_gem_mode?
|
|
107
|
+
|
|
108
|
+
gemfiles_dir = 'gemfiles'
|
|
109
|
+
return nil unless File.directory?(gemfiles_dir)
|
|
110
|
+
|
|
111
|
+
# Look for any file containing the version pattern, exclude lock files
|
|
112
|
+
matching_files = Dir.glob(File.join(gemfiles_dir, '*')).select do |f|
|
|
113
|
+
basename = File.basename(f)
|
|
114
|
+
rails_version_regex = rails_version.gsub('.', '[.-]')
|
|
115
|
+
basename =~ /#{rails_version_regex}/ && basename !~ /\.lock$/
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
raise Error, "No gemfile found in gemfiles/ for Rails #{rails_version}" if matching_files.empty?
|
|
119
|
+
|
|
120
|
+
if matching_files.length > 1
|
|
121
|
+
# Prefer exact patterns, warn about multiple matches
|
|
122
|
+
puts "⚠️ Warning: Multiple gemfiles found for Rails #{rails_version}:"
|
|
123
|
+
matching_files.each { |f| puts " - #{File.basename(f)}" }
|
|
124
|
+
puts " Using: #{File.basename(matching_files.first)}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
File.basename(matching_files.first)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# Reads an Aptfile from the gem root (one package per line, # comments allowed).
|
|
133
|
+
# Returns the packages as a space-separated string, or '' if no Aptfile exists.
|
|
134
|
+
def extra_apt_packages
|
|
135
|
+
return '' unless target_gem_mode?
|
|
136
|
+
|
|
137
|
+
aptfile = File.join(expanded_gem_path, 'Aptfile')
|
|
138
|
+
return '' unless File.exist?(aptfile)
|
|
139
|
+
|
|
140
|
+
packages = File.readlines(aptfile)
|
|
141
|
+
.map { |l| l.sub(/#.*/, '').strip }
|
|
142
|
+
.reject(&:empty?)
|
|
143
|
+
packages.join(' ')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns the Dockerfile RUN lines needed to install the requested database adapter,
|
|
147
|
+
# stripping any existing declaration first to avoid version conflicts.
|
|
148
|
+
# Returns an empty string when no --db was specified (gem's own Gemfile is used as-is).
|
|
149
|
+
def adapter_setup
|
|
150
|
+
return '' unless database
|
|
151
|
+
|
|
152
|
+
gem_name, version, sed_pattern = case database.to_s
|
|
153
|
+
when 'mysql' then ['mysql2', '~> 0.5', '/mysql2/d']
|
|
154
|
+
when 'postgres' then ['pg', '~> 1.1', "/'pg'/d; /\\\"pg\\\"/d"]
|
|
155
|
+
else ['sqlite3', '~> 2.1', '/sqlite3/d']
|
|
156
|
+
end
|
|
157
|
+
"RUN sed -i \"#{sed_pattern}\" Gemfile\nRUN echo \"gem '#{gem_name}', '#{version}'\" >> Gemfile"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_docker!
|
|
161
|
+
return if system('docker --version > /dev/null 2>&1')
|
|
162
|
+
|
|
163
|
+
raise Error, 'Docker is not installed or not in PATH'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def validate_gem_path!
|
|
167
|
+
expanded = File.expand_path(gem_path)
|
|
168
|
+
raise Error, "Gem path does not exist: #{expanded}" unless File.directory?(expanded)
|
|
169
|
+
|
|
170
|
+
# Check if gem has gemfiles/ directory - should use local mode instead
|
|
171
|
+
gemfiles_dir = File.join(expanded, 'gemfiles')
|
|
172
|
+
return unless File.directory?(gemfiles_dir)
|
|
173
|
+
|
|
174
|
+
raise Error, <<~ERROR
|
|
175
|
+
This gem has a 'gemfiles/' directory and should be tested in local mode.
|
|
176
|
+
|
|
177
|
+
Instead of:
|
|
178
|
+
railstest --gem-path #{gem_path}
|
|
179
|
+
|
|
180
|
+
Use local mode by running from the gem directory:
|
|
181
|
+
cd #{gem_path}
|
|
182
|
+
railstest
|
|
183
|
+
ERROR
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def extract_gem_name(gemspec_path)
|
|
187
|
+
gemspec_files = Dir.glob(File.join(gemspec_path, '*.gemspec'))
|
|
188
|
+
|
|
189
|
+
raise Error, "No .gemspec file found in #{gemspec_path}" if gemspec_files.empty?
|
|
190
|
+
|
|
191
|
+
gemspec_file = gemspec_files.first
|
|
192
|
+
content = File.read(gemspec_file)
|
|
193
|
+
|
|
194
|
+
# Try to match spec.name = "gem_name" or s.name = 'gem_name' (handles both spec and s)
|
|
195
|
+
return ::Regexp.last_match(1) if content =~ /\w+\.name\s*=\s*["']([^"']+)["']/
|
|
196
|
+
|
|
197
|
+
raise Error, "Could not extract gem name from #{gemspec_file}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def generate_dockerfile
|
|
201
|
+
if target_gem_mode?
|
|
202
|
+
generate_target_gem_dockerfile
|
|
203
|
+
else
|
|
204
|
+
generate_local_dockerfile
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def generate_target_gem_dockerfile
|
|
209
|
+
has_dummy_app = File.exist?(File.join(expanded_gem_path, 'test/dummy/config/environment.rb'))
|
|
210
|
+
|
|
211
|
+
if has_dummy_app
|
|
212
|
+
# Rails engine with dummy app - mount gem and test from within it
|
|
213
|
+
<<~DOCKERFILE
|
|
214
|
+
# Dockerfile for running Rails engine tests
|
|
215
|
+
|
|
216
|
+
ARG RUBY_VERSION=3.3
|
|
217
|
+
FROM ruby:$RUBY_VERSION
|
|
218
|
+
|
|
219
|
+
ARG RAILS_VERSION
|
|
220
|
+
|
|
221
|
+
RUN apt-get update -qq && apt-get install -y build-essential git curl #{extra_apt_packages}
|
|
222
|
+
|
|
223
|
+
WORKDIR /app/test_app
|
|
224
|
+
|
|
225
|
+
# Install Rails version needed by the gem
|
|
226
|
+
RUN gem install rails --version "~> ${RAILS_VERSION}.0" --no-document
|
|
227
|
+
|
|
228
|
+
# Copy all gem files
|
|
229
|
+
COPY . .
|
|
230
|
+
|
|
231
|
+
RUN rm -f Gemfile.lock
|
|
232
|
+
#{adapter_setup}
|
|
233
|
+
|
|
234
|
+
RUN gem install bundler --quiet --no-document && bundle install --quiet
|
|
235
|
+
DOCKERFILE
|
|
236
|
+
else
|
|
237
|
+
# Regular Rails app or library - create new test app
|
|
238
|
+
<<~DOCKERFILE
|
|
239
|
+
# Dockerfile for running gem tests
|
|
240
|
+
|
|
241
|
+
ARG RUBY_VERSION=3.3
|
|
242
|
+
FROM ruby:$RUBY_VERSION
|
|
243
|
+
|
|
244
|
+
ARG RAILS_VERSION
|
|
245
|
+
ARG TARGET_GEM_NAME
|
|
246
|
+
|
|
247
|
+
RUN apt-get update -qq && apt-get install -y build-essential git curl
|
|
248
|
+
|
|
249
|
+
WORKDIR /app
|
|
250
|
+
|
|
251
|
+
# Create a new Rails application to host the gem tests
|
|
252
|
+
# Use full Rails (not minimal/api) since gem could extend any part of Rails
|
|
253
|
+
RUN gem install rails --version "~> ${RAILS_VERSION}.0" --no-document --quiet
|
|
254
|
+
RUN rails new test_app --skip-bundle
|
|
255
|
+
|
|
256
|
+
WORKDIR /app/test_app
|
|
257
|
+
|
|
258
|
+
#{adapter_setup}
|
|
259
|
+
|
|
260
|
+
# Remove existing gem entry if present, then add target gem with path
|
|
261
|
+
RUN sed -i "/gem ['\"]${TARGET_GEM_NAME}['\"]/d" Gemfile && \
|
|
262
|
+
echo "gem '${TARGET_GEM_NAME}', path: '/app/target_gem'" >> Gemfile
|
|
263
|
+
|
|
264
|
+
# Install dependencies - this gets cached if Gemfile doesn't change
|
|
265
|
+
RUN bundle install --quiet
|
|
266
|
+
DOCKERFILE
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def generate_local_dockerfile
|
|
271
|
+
<<~DOCKERFILE
|
|
272
|
+
# Dockerfile for running tests
|
|
273
|
+
|
|
274
|
+
ARG RUBY_VERSION=3.3
|
|
275
|
+
FROM ruby:$RUBY_VERSION
|
|
276
|
+
|
|
277
|
+
ARG GEMFILE_PATH
|
|
278
|
+
|
|
279
|
+
RUN apt-get update -qq && apt-get install -y build-essential
|
|
280
|
+
|
|
281
|
+
WORKDIR /app
|
|
282
|
+
COPY . .
|
|
283
|
+
|
|
284
|
+
# Use the actual gemfile found in gemfiles/ directory
|
|
285
|
+
RUN BUNDLE_GEMFILE="${GEMFILE_PATH}" bundle install --quiet
|
|
286
|
+
|
|
287
|
+
# No ENTRYPOINT - commands will be passed directly to docker run
|
|
288
|
+
DOCKERFILE
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railstest
|
|
4
|
+
SUPPORTED_VERSIONS = {
|
|
5
|
+
ruby: {
|
|
6
|
+
minimum: '3.2',
|
|
7
|
+
recommended: '4.0', # Latest stable
|
|
8
|
+
supported: ['3.2', '3.3', '3.4', '4.0'],
|
|
9
|
+
experimental: ['2.5', '2.6', '2.7', '3.0', '3.1'] # EOL
|
|
10
|
+
},
|
|
11
|
+
rails: {
|
|
12
|
+
minimum: '7.0',
|
|
13
|
+
recommended: '8.1', # Latest stable
|
|
14
|
+
supported: ['7.2', '8.0', '8.1'],
|
|
15
|
+
experimental: ['6.0', '6.1', '7.0', '7.1'] # EOL
|
|
16
|
+
},
|
|
17
|
+
# Ruby version => Working Rails versions
|
|
18
|
+
# Verified combinations from self-testing + known requirements from Rails
|
|
19
|
+
#
|
|
20
|
+
# Ruby < 3.2 is intentionally absent: every Rails version we target depends
|
|
21
|
+
# on zeitwerk, and zeitwerk >= 2.7.0 requires Ruby >= 3.2. A fresh
|
|
22
|
+
# `gem install rails` resolves to the latest zeitwerk, so it can no longer
|
|
23
|
+
# be installed on Ruby < 3.2 regardless of the Rails version requested.
|
|
24
|
+
compatibility: {
|
|
25
|
+
'4.0' => ['7.1', '7.2', '8.0', '8.1'], # Rails 8.x requires Ruby >= 3.2
|
|
26
|
+
'3.4' => ['7.1', '7.2', '8.0', '8.1'], # Rails 8.x requires Ruby >= 3.2
|
|
27
|
+
'3.3' => ['7.0', '7.1', '7.2', '8.0', '8.1'], # Rails 8.x requires Ruby >= 3.2, verified 7.x-8.0
|
|
28
|
+
'3.2' => ['7.0', '7.1', '7.2', '8.0', '8.1'], # Rails 8.x requires Ruby >= 3.2, verified 7.x-8.0
|
|
29
|
+
# zeitwerk >= 2.7.0 requires Ruby >= 3.2, so these can no longer
|
|
30
|
+
# fresh-install any zeitwerk-based Rails (6.0+).
|
|
31
|
+
'3.1' => [],
|
|
32
|
+
'3.0' => [],
|
|
33
|
+
'2.7' => [],
|
|
34
|
+
'2.6' => [],
|
|
35
|
+
'2.5' => []
|
|
36
|
+
},
|
|
37
|
+
notes: {
|
|
38
|
+
'2.5' => 'Unsupported: Rails depends on zeitwerk, which requires Ruby >= 3.2',
|
|
39
|
+
'2.6' => 'Unsupported: Rails depends on zeitwerk, which requires Ruby >= 3.2',
|
|
40
|
+
'2.7' => 'Unsupported: Rails depends on zeitwerk, which requires Ruby >= 3.2',
|
|
41
|
+
'3.0' => 'Unsupported: Rails depends on zeitwerk, which requires Ruby >= 3.2',
|
|
42
|
+
'3.1' => 'Unsupported: Rails depends on zeitwerk, which requires Ruby >= 3.2',
|
|
43
|
+
'8.0' => 'Requires Ruby 3.2+',
|
|
44
|
+
'8.1' => 'Requires Ruby 3.2+'
|
|
45
|
+
}
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
def self.compatible?(ruby_version, rails_version)
|
|
49
|
+
ruby_major_minor = normalize_version(ruby_version)
|
|
50
|
+
rails_major_minor = normalize_version(rails_version)
|
|
51
|
+
|
|
52
|
+
compat = SUPPORTED_VERSIONS[:compatibility][ruby_major_minor]
|
|
53
|
+
return nil unless compat # Unknown Ruby version
|
|
54
|
+
|
|
55
|
+
compat.include?(rails_major_minor)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.recommended_rails_versions(ruby_version)
|
|
59
|
+
ruby_major_minor = normalize_version(ruby_version)
|
|
60
|
+
SUPPORTED_VERSIONS[:compatibility][ruby_major_minor] || []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.note_for(version)
|
|
64
|
+
major_minor = normalize_version(version)
|
|
65
|
+
SUPPORTED_VERSIONS[:notes][major_minor]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.normalize_version(version)
|
|
69
|
+
# Convert "3.3.0" or "3.3" to "3.3"
|
|
70
|
+
version.to_s.match(/^(\d+\.\d+)/)[1]
|
|
71
|
+
rescue NoMethodError
|
|
72
|
+
version.to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
module Railstest
|
|
5
|
+
class TestRunner
|
|
6
|
+
attr_reader :options, :docker_manager, :database_manager
|
|
7
|
+
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
@docker_manager = DockerManager.new(
|
|
11
|
+
ruby_version: options[:ruby_version],
|
|
12
|
+
rails_version: options[:rails_version],
|
|
13
|
+
gem_path: options[:gem_path],
|
|
14
|
+
database: options[:database]
|
|
15
|
+
)
|
|
16
|
+
@database_manager = DatabaseManager.new(database: options[:database])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
# Setup signal handlers for graceful cleanup on Ctrl+C
|
|
21
|
+
setup_signal_handlers
|
|
22
|
+
|
|
23
|
+
# Validate that tests exist before building Docker image
|
|
24
|
+
validate_tests_exist!
|
|
25
|
+
|
|
26
|
+
docker_manager.build_image
|
|
27
|
+
database_manager.start
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
database_manager.setup_database(docker_manager)
|
|
31
|
+
exit_status = run_tests
|
|
32
|
+
exit_status
|
|
33
|
+
ensure
|
|
34
|
+
database_manager.stop
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_tests_exist!
|
|
41
|
+
gem_path = if docker_manager.target_gem_mode?
|
|
42
|
+
docker_manager.expanded_gem_path
|
|
43
|
+
else
|
|
44
|
+
Dir.pwd
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check for test or spec directories
|
|
48
|
+
test_dir = File.join(gem_path, 'test')
|
|
49
|
+
spec_dir = File.join(gem_path, 'spec')
|
|
50
|
+
|
|
51
|
+
has_test_dir = File.directory?(test_dir)
|
|
52
|
+
has_spec_dir = File.directory?(spec_dir)
|
|
53
|
+
|
|
54
|
+
unless has_test_dir || has_spec_dir
|
|
55
|
+
raise Railstest::Error, <<~ERROR
|
|
56
|
+
No test directory found in gem.
|
|
57
|
+
|
|
58
|
+
Railstest requires either a 'test/' or 'spec/' directory with tests.
|
|
59
|
+
|
|
60
|
+
Gem path: #{gem_path}
|
|
61
|
+
ERROR
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if the directories actually contain test files
|
|
65
|
+
test_files = []
|
|
66
|
+
test_files += Dir.glob(File.join(test_dir, '**/*_test.rb')) if has_test_dir
|
|
67
|
+
test_files += Dir.glob(File.join(spec_dir, '**/*_spec.rb')) if has_spec_dir
|
|
68
|
+
|
|
69
|
+
return unless test_files.empty?
|
|
70
|
+
|
|
71
|
+
raise Railstest::Error, <<~ERROR
|
|
72
|
+
No test files found in gem.
|
|
73
|
+
|
|
74
|
+
Found directories:
|
|
75
|
+
#{has_test_dir ? ' - test/' : ''}
|
|
76
|
+
#{has_spec_dir ? ' - spec/' : ''}
|
|
77
|
+
|
|
78
|
+
But no test files (*_test.rb or *_spec.rb) were found.
|
|
79
|
+
|
|
80
|
+
Gem path: #{gem_path}
|
|
81
|
+
ERROR
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup_signal_handlers
|
|
85
|
+
# Trap SIGINT (Ctrl+C) and SIGTERM to ensure cleanup
|
|
86
|
+
%w[INT TERM].each do |signal|
|
|
87
|
+
Signal.trap(signal) do
|
|
88
|
+
puts "\n\nInterrupted! Cleaning up..."
|
|
89
|
+
database_manager.stop
|
|
90
|
+
exit(130) # Standard exit code for SIGINT
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_tests
|
|
96
|
+
test_framework = detect_test_framework
|
|
97
|
+
command = build_test_command(test_framework)
|
|
98
|
+
|
|
99
|
+
puts "Running tests with #{options[:database]}..."
|
|
100
|
+
|
|
101
|
+
# Use IO.popen to stream output in real-time
|
|
102
|
+
# Read in chunks to show test dots as they appear (not line-buffered)
|
|
103
|
+
IO.popen(command, err: %i[child out]) do |io|
|
|
104
|
+
loop do
|
|
105
|
+
chunk = io.readpartial(1024)
|
|
106
|
+
print chunk
|
|
107
|
+
$stdout.flush
|
|
108
|
+
rescue EOFError
|
|
109
|
+
break
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
$CHILD_STATUS.exitstatus
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def detect_test_framework
|
|
117
|
+
# Check for spec directory
|
|
118
|
+
if docker_manager.target_gem_mode?
|
|
119
|
+
gem_path = docker_manager.expanded_gem_path
|
|
120
|
+
return :rspec if File.directory?(File.join(gem_path, 'spec'))
|
|
121
|
+
return :rails_test if File.directory?(File.join(gem_path, 'test'))
|
|
122
|
+
|
|
123
|
+
# Check Gemfile for rspec
|
|
124
|
+
gemfile_path = File.join(gem_path, 'Gemfile')
|
|
125
|
+
if File.exist?(gemfile_path)
|
|
126
|
+
gemfile_content = File.read(gemfile_path)
|
|
127
|
+
return :rspec if gemfile_content =~ /gem\s+['"]rspec/
|
|
128
|
+
end
|
|
129
|
+
else
|
|
130
|
+
return :rspec if File.directory?('spec')
|
|
131
|
+
return :rails_test if File.directory?('test')
|
|
132
|
+
|
|
133
|
+
if File.exist?('Gemfile')
|
|
134
|
+
gemfile_content = File.read('Gemfile')
|
|
135
|
+
return :rspec if gemfile_content =~ /gem\s+['"]rspec/
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Default to rails_test
|
|
140
|
+
:rails_test
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_test_command(test_framework)
|
|
144
|
+
cmd = ['docker', 'run', '--rm', '--network=host']
|
|
145
|
+
|
|
146
|
+
# Environment variables
|
|
147
|
+
cmd << '-e' << "DATABASE=#{options[:database]}"
|
|
148
|
+
cmd << '-e' << "TARGET_DB=#{options[:database]}"
|
|
149
|
+
cmd << '-e' << 'RAILS_ENV=test'
|
|
150
|
+
|
|
151
|
+
# Volume mounts and working directory for target gem mode
|
|
152
|
+
if docker_manager.target_gem_mode?
|
|
153
|
+
has_dummy_app = File.exist?(File.join(docker_manager.expanded_gem_path, 'test/dummy/config/environment.rb'))
|
|
154
|
+
|
|
155
|
+
if has_dummy_app
|
|
156
|
+
# Rails engine: already baked into image via COPY during build - no volume mount needed
|
|
157
|
+
else
|
|
158
|
+
# Regular app: use target-gem volume mount
|
|
159
|
+
cmd << '-v' << "#{docker_manager.expanded_gem_path}:/app/target_gem"
|
|
160
|
+
end
|
|
161
|
+
cmd << '-w' << '/app/test_app'
|
|
162
|
+
else
|
|
163
|
+
# Local mode: use actual gemfile found in gemfiles/
|
|
164
|
+
gemfile = docker_manager.find_gemfile_for_version
|
|
165
|
+
cmd << '-e' << "BUNDLE_GEMFILE=/app/gemfiles/#{gemfile}"
|
|
166
|
+
cmd << '-w' << '/app'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
cmd << docker_manager.image_name
|
|
170
|
+
|
|
171
|
+
# In target-gem mode, bundle is already installed during Docker build
|
|
172
|
+
if docker_manager.target_gem_mode?
|
|
173
|
+
test_cmd = build_test_subcommand(test_framework)
|
|
174
|
+
cmd << 'bash' << '-c' << test_cmd.to_s
|
|
175
|
+
else
|
|
176
|
+
# Local mode - bundle already installed during build
|
|
177
|
+
cmd.concat(build_test_subcommand_array(test_framework))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
cmd
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def build_test_subcommand(test_framework)
|
|
184
|
+
# Returns a shell command string for target-gem mode
|
|
185
|
+
has_dummy_app = File.exist?(File.join(docker_manager.expanded_gem_path, 'test/dummy/config/environment.rb'))
|
|
186
|
+
|
|
187
|
+
case test_framework
|
|
188
|
+
when :rspec
|
|
189
|
+
test_path = if has_dummy_app
|
|
190
|
+
options[:test_path] ? remap_path_for_container(options[:test_path]) : '/app/test_app/spec'
|
|
191
|
+
else
|
|
192
|
+
options[:test_path] ? remap_path_for_container(options[:test_path]) : '/app/target_gem/spec'
|
|
193
|
+
end
|
|
194
|
+
"bundle exec rspec #{test_path}"
|
|
195
|
+
when :rails_test
|
|
196
|
+
if has_dummy_app
|
|
197
|
+
# Rails engine: just run bin/rails test (discovers tests in dummy app)
|
|
198
|
+
options[:test_path] ? "bin/rails test #{remap_path_for_container(options[:test_path])}" : 'bin/rails test'
|
|
199
|
+
else
|
|
200
|
+
test_path = options[:test_path] ? remap_path_for_container(options[:test_path]) : '/app/target_gem/test'
|
|
201
|
+
"bin/rails test #{test_path}"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_test_subcommand_array(test_framework)
|
|
207
|
+
# Returns an array of command parts for local mode
|
|
208
|
+
case test_framework
|
|
209
|
+
when :rspec
|
|
210
|
+
cmd = %w[bundle exec rspec]
|
|
211
|
+
cmd << options[:test_path] if options[:test_path]
|
|
212
|
+
cmd
|
|
213
|
+
when :rails_test
|
|
214
|
+
# Check if bin/rails exists, otherwise use bundle exec rails
|
|
215
|
+
cmd = if File.exist?('bin/rails')
|
|
216
|
+
['bin/rails', 'test']
|
|
217
|
+
else
|
|
218
|
+
%w[bundle exec rails test]
|
|
219
|
+
end
|
|
220
|
+
cmd << options[:test_path] if options[:test_path]
|
|
221
|
+
cmd
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def remap_path_for_container(path)
|
|
226
|
+
# In target-gem mode, --path is interpreted relative to gem root
|
|
227
|
+
return path unless docker_manager.target_gem_mode?
|
|
228
|
+
|
|
229
|
+
# Always interpret as relative to gem root for consistency
|
|
230
|
+
# Remove leading slash if present to treat as relative
|
|
231
|
+
clean_path = path.start_with?('/') ? path[1..] : path
|
|
232
|
+
|
|
233
|
+
"/app/target_gem/#{clean_path}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
data/lib/railstest.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'railstest/version'
|
|
4
|
+
require_relative 'railstest/supported_versions'
|
|
5
|
+
require_relative 'railstest/docker_manager'
|
|
6
|
+
require_relative 'railstest/database_manager'
|
|
7
|
+
require_relative 'railstest/test_runner'
|
|
8
|
+
require_relative 'railstest/cli'
|
|
9
|
+
|
|
10
|
+
module Railstest
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: railstest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Runar Ingebrigtsen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Test Rails gems with various Ruby, Rails, and database combinations using
|
|
13
|
+
Docker
|
|
14
|
+
email:
|
|
15
|
+
- ringe@rin.no
|
|
16
|
+
executables:
|
|
17
|
+
- railstest
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- bin/railstest
|
|
25
|
+
- docker-compose.yml
|
|
26
|
+
- lib/railstest.rb
|
|
27
|
+
- lib/railstest/cli.rb
|
|
28
|
+
- lib/railstest/database_manager.rb
|
|
29
|
+
- lib/railstest/docker_manager.rb
|
|
30
|
+
- lib/railstest/supported_versions.rb
|
|
31
|
+
- lib/railstest/test_runner.rb
|
|
32
|
+
- lib/railstest/version.rb
|
|
33
|
+
homepage: https://github.com/ringe/railstest
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/ringe/railstest
|
|
38
|
+
source_code_uri: https://github.com/ringe/railstest
|
|
39
|
+
changelog_uri: https://github.com/ringe/railstest/blob/main/CHANGELOG.md
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 4.0.11
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Docker-based testing tool for Rails gems
|
|
57
|
+
test_files: []
|