ruby_raider 3.9.0 → 3.11.0
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 +4 -4
- data/.github/workflows/release.yml +18 -29
- data/.github/workflows/security.yml +68 -0
- data/.rubocop.yml +28 -0
- data/README.md +23 -4
- data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +400 -0
- data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +233 -0
- data/docs/superpowers/plans/2026-10-28-failure-card.md +295 -0
- data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +214 -0
- data/docs/superpowers/specs/2026-10-28-failure-card-design.md +210 -0
- data/lib/commands/scaffolding_commands.rb +3 -3
- data/lib/commands/utility_commands.rb +36 -6
- data/lib/generators/cucumber/templates/cucumber.tt +1 -1
- data/lib/generators/cucumber/templates/feature.tt +5 -0
- data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/helper_generator.rb +12 -1
- data/lib/generators/infrastructure/azure_generator.rb +1 -0
- data/lib/generators/infrastructure/github_generator.rb +1 -0
- data/lib/generators/infrastructure/templates/azure_pipelines.tt +12 -1
- data/lib/generators/infrastructure/templates/github.tt +12 -0
- data/lib/generators/infrastructure/templates/github_appium.tt +23 -0
- data/lib/generators/invoke_generators.rb +20 -2
- data/lib/generators/rspec/rspec_generator.rb +0 -1
- data/lib/generators/rspec/templates/spec.tt +12 -0
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/git_ignore.tt +7 -1
- data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
- data/lib/generators/templates/common/partials/web_config.tt +9 -1
- data/lib/generators/templates/common/read_me.tt +84 -2
- data/lib/generators/templates/helpers/browser_helper.tt +5 -1
- data/lib/generators/templates/helpers/driver_helper.tt +1 -0
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
- data/lib/generators/templates/helpers/session_state.tt +206 -0
- data/lib/generators/templates/helpers/spec_helper.tt +28 -0
- data/lib/generators/templates/helpers/video_helper.tt +338 -171
- data/lib/ruby_raider/ffmpeg.rb +280 -0
- data/lib/ruby_raider.rb +2 -3
- data/lib/scaffolding/project_detector.rb +1 -3
- data/lib/scaffolding/scaffolding.rb +0 -1
- data/lib/utilities/utilities.rb +15 -2
- data/lib/version +1 -1
- data/ruby_raider.gemspec +4 -1
- data/sig/commands/utility_commands.rbs +4 -1
- data/sig/generators/invoke_generators.rbs +2 -3
- data/sig/ruby_raider/ffmpeg.rbs +23 -0
- data/sig/utilities/utilities.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +56 -1
- data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
- data/spec/generators/video_helper_spec.rb +608 -0
- data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
- data/spec/integration/commands/utility_commands_spec.rb +2 -2
- data/spec/integration/content/azure_content_spec.rb +33 -0
- data/spec/integration/content/ci_content_spec.rb +54 -0
- data/spec/integration/content/common_content_spec.rb +35 -0
- data/spec/integration/content/config_content_spec.rb +32 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +47 -1
- data/spec/integration/content/gemfile_content_spec.rb +2 -3
- data/spec/integration/content/helper_content_spec.rb +178 -0
- data/spec/integration/content/page_content_spec.rb +0 -1
- data/spec/integration/content/readme_content_spec.rb +79 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/syntax_validation_spec.rb +15 -0
- data/spec/integration/content/test_content_spec.rb +27 -3
- data/spec/integration/end_to_end_features_spec.rb +0 -2
- data/spec/integration/end_to_end_spec.rb +275 -26
- data/spec/integration/generators/azure_generator_spec.rb +7 -6
- data/spec/integration/generators/common_generator_spec.rb +1 -1
- data/spec/integration/generators/helpers_generator_spec.rb +22 -0
- data/spec/integration/generators/invoke_generators_spec.rb +52 -0
- data/spec/integration/helpers/session_state_spec.rb +428 -0
- data/spec/integration/scaffolding_e2e_spec.rb +13 -24
- data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
- data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
- data/spec/system/selenium_spec.rb +0 -1
- data/spec/system/watir_spec.rb +0 -1
- data/spec/utilities/ffmpeg_spec.rb +424 -0
- data/spec/utilities/utilities_spec.rb +19 -2
- data/spec/utilities/video_command_spec.rb +120 -0
- metadata +65 -11
- data/lib/commands/adopt_commands.rb +0 -42
- data/lib/commands/loaded_commands.rb +0 -9
- data/lib/commands/plugin_commands.rb +0 -32
- data/sig/commands/adopt_commands.rbs +0 -8
- data/sig/commands/loaded_commands.rbs +0 -5
- data/sig/commands/plugin_commands.rbs +0 -9
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'rbconfig'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'uri'
|
|
9
|
+
|
|
10
|
+
module RubyRaider
|
|
11
|
+
# Pinned, checksum-verified ffmpeg provisioning. Stdlib only so generated
|
|
12
|
+
# projects can load it standalone via `require 'ruby_raider/ffmpeg'`.
|
|
13
|
+
module Ffmpeg
|
|
14
|
+
MAX_REDIRECTS = 5
|
|
15
|
+
# Seconds for the whole download, redirects included.
|
|
16
|
+
DOWNLOAD_TIMEOUT = 300
|
|
17
|
+
|
|
18
|
+
# Checksums are filled in when the RaiderHQ `ffmpeg-7.1` release is cut.
|
|
19
|
+
RELEASE_BASE = 'https://github.com/RaiderHQ/ruby_raider/releases/download/ffmpeg-7.1/ffmpeg-7.1-'
|
|
20
|
+
MANIFEST = {
|
|
21
|
+
version: '7.1',
|
|
22
|
+
assets: {
|
|
23
|
+
'darwin-arm64' => { url: "#{RELEASE_BASE}darwin-arm64".freeze, sha256: nil }.freeze,
|
|
24
|
+
'darwin-x64' => { url: "#{RELEASE_BASE}darwin-x64".freeze, sha256: nil }.freeze,
|
|
25
|
+
'linux-x64' => { url: "#{RELEASE_BASE}linux-x64".freeze, sha256: nil }.freeze,
|
|
26
|
+
'windows-x64' => { url: "#{RELEASE_BASE}windows-x64.exe".freeze, sha256: nil }.freeze
|
|
27
|
+
}.freeze
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
PLATFORMS = %w[darwin-arm64 darwin-x64 linux-x64 windows-x64].freeze
|
|
31
|
+
# $stderr.puts, not Kernel#warn: warn is silenced under -W0 / $VERBOSE = nil.
|
|
32
|
+
# rubocop:disable Style/StderrPuts
|
|
33
|
+
DEFAULT_WARN = ->(message) { $stderr.puts("[video] #{message}") }
|
|
34
|
+
# rubocop:enable Style/StderrPuts
|
|
35
|
+
|
|
36
|
+
@verified = {}
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def platform(host_os: RbConfig::CONFIG['host_os'], host_cpu: RbConfig::CONFIG['host_cpu'])
|
|
40
|
+
os = host_os_name(host_os)
|
|
41
|
+
cpu = host_cpu_name(host_cpu)
|
|
42
|
+
return nil unless os && cpu
|
|
43
|
+
|
|
44
|
+
candidate = "#{os}-#{cpu}"
|
|
45
|
+
PLATFORMS.include?(candidate) ? candidate : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cache_dir(env: ENV, platform: self.platform)
|
|
49
|
+
override = env['RAIDER_CACHE_DIR']
|
|
50
|
+
return override if override && !override.empty?
|
|
51
|
+
|
|
52
|
+
local = env['LOCALAPPDATA']
|
|
53
|
+
if platform == 'windows-x64' && local && !local.empty?
|
|
54
|
+
File.join(local, 'raider')
|
|
55
|
+
else
|
|
56
|
+
File.join(Dir.home, '.cache', 'raider')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cached_path(env: ENV, manifest: MANIFEST, platform: self.platform)
|
|
61
|
+
name = platform.to_s.start_with?('windows') ? 'ffmpeg.exe' : 'ffmpeg'
|
|
62
|
+
File.join(cache_dir(env:, platform:), 'ffmpeg', manifest[:version], name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# No network: the env override if executable, else the verified cached binary.
|
|
66
|
+
# Never raises: any StandardError becomes one warning and nil.
|
|
67
|
+
def resolve(env: ENV, manifest: MANIFEST, warn: nil, platform: nil)
|
|
68
|
+
warn ||= DEFAULT_WARN
|
|
69
|
+
guarded(warn) do
|
|
70
|
+
lookup(env, manifest, warn, platform || self.platform).first
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Never raises: any StandardError becomes one warning and nil.
|
|
75
|
+
def ensure!(env: ENV, manifest: MANIFEST, fetch: nil, warn: nil, platform: nil)
|
|
76
|
+
warn ||= DEFAULT_WARN
|
|
77
|
+
guarded(warn) do
|
|
78
|
+
platform ||= self.platform
|
|
79
|
+
path, final = lookup(env, manifest, warn, platform)
|
|
80
|
+
next path if path || final
|
|
81
|
+
|
|
82
|
+
provision(env, manifest, fetch || method(:http_get), warn, platform)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Cheap check, no network and no hashing: can this run get an ffmpeg at all?
|
|
87
|
+
# True for an executable RAIDER_FFMPEG_PATH or a checksummed asset for the platform.
|
|
88
|
+
def obtainable?(env: ENV, manifest: MANIFEST, platform: nil)
|
|
89
|
+
override = env['RAIDER_FFMPEG_PATH']
|
|
90
|
+
return true if override && !override.empty? && executable_file?(override)
|
|
91
|
+
|
|
92
|
+
!manifest[:assets][(platform || self.platform).to_s]&.fetch(:sha256, nil).nil?
|
|
93
|
+
rescue StandardError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def http_get(url, destination, limit = MAX_REDIRECTS, timeout: DOWNLOAD_TIMEOUT, deadline: nil)
|
|
98
|
+
raise ArgumentError, "refusing non-https url: #{url}" unless https?(url)
|
|
99
|
+
raise IOError, 'too many redirects' if limit.negative?
|
|
100
|
+
|
|
101
|
+
deadline ||= monotonic_now + timeout
|
|
102
|
+
uri = URI(url)
|
|
103
|
+
location = nil
|
|
104
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true, **socket_timeouts(deadline, timeout)) do |http|
|
|
105
|
+
http.request(Net::HTTP::Get.new(uri)) do |response|
|
|
106
|
+
location = handle_response(response, destination, deadline, timeout)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
http_get(URI.join(url, location).to_s, destination, limit - 1, timeout:, deadline:) if location
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Returns the redirect location, or nil once the body is written.
|
|
115
|
+
def handle_response(response, destination, deadline, timeout)
|
|
116
|
+
case response
|
|
117
|
+
when Net::HTTPSuccess
|
|
118
|
+
File.open(destination, 'wb') do |file|
|
|
119
|
+
response.read_body do |chunk|
|
|
120
|
+
check_deadline(deadline, timeout)
|
|
121
|
+
file.write(chunk)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
nil
|
|
125
|
+
when Net::HTTPRedirection
|
|
126
|
+
response['location'] || raise(IOError, 'redirect without Location header')
|
|
127
|
+
else
|
|
128
|
+
raise IOError, "download failed: HTTP #{response.code}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def socket_timeouts(deadline, timeout)
|
|
133
|
+
left = check_deadline(deadline, timeout)
|
|
134
|
+
{ open_timeout: [10, left].min, read_timeout: [60, left].min }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def check_deadline(deadline, timeout)
|
|
138
|
+
left = deadline - monotonic_now
|
|
139
|
+
raise IOError, "download took longer than #{timeout} seconds" unless left.positive?
|
|
140
|
+
|
|
141
|
+
left
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def monotonic_now
|
|
145
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def guarded(warn)
|
|
149
|
+
yield
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
warn_nil(warn, "ffmpeg unavailable: #{e.message}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reset_verified_cache!
|
|
155
|
+
@verified.clear
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def host_os_name(host_os)
|
|
159
|
+
case host_os
|
|
160
|
+
when /darwin/ then 'darwin'
|
|
161
|
+
when /linux/ then 'linux'
|
|
162
|
+
when /mswin|mingw|cygwin|windows/ then 'windows'
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def host_cpu_name(host_cpu)
|
|
167
|
+
case host_cpu
|
|
168
|
+
when /arm64|aarch64/ then 'arm64'
|
|
169
|
+
when /x86_64|x64|amd64/ then 'x64'
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns [path_or_nil, final]. final means "do not try to download".
|
|
174
|
+
def lookup(env, manifest, warn, platform)
|
|
175
|
+
override = env['RAIDER_FFMPEG_PATH']
|
|
176
|
+
if override && !override.empty?
|
|
177
|
+
return [override, true] if executable_file?(override)
|
|
178
|
+
|
|
179
|
+
warn.call('RAIDER_FFMPEG_PATH is not an executable file; ignoring')
|
|
180
|
+
end
|
|
181
|
+
cached_lookup(env, manifest, warn, platform)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def cached_lookup(env, manifest, warn, platform)
|
|
185
|
+
sha = manifest[:assets][platform.to_s]&.fetch(:sha256, nil)
|
|
186
|
+
path = cached_path(env:, manifest:, platform:)
|
|
187
|
+
return [nil, false] unless platform && sha && executable_file?(path)
|
|
188
|
+
return [path, true] if verified_cached?(path, sha)
|
|
189
|
+
|
|
190
|
+
FileUtils.rm_f(path)
|
|
191
|
+
warn.call('cached ffmpeg failed checksum; video disabled')
|
|
192
|
+
[nil, true]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def verified_cached?(path, sha256)
|
|
196
|
+
key = verification_key(path, sha256)
|
|
197
|
+
return true if @verified[key]
|
|
198
|
+
|
|
199
|
+
ok = verified?(path, sha256)
|
|
200
|
+
@verified[key] = true if ok
|
|
201
|
+
ok
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def verification_key(path, sha256)
|
|
205
|
+
stat = File.stat(path)
|
|
206
|
+
[path, stat.ino, stat.mtime.to_f, stat.ctime.to_f, stat.size, sha256]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def provision(env, manifest, fetch, warn, platform)
|
|
210
|
+
asset = manifest[:assets][platform.to_s]
|
|
211
|
+
unless asset
|
|
212
|
+
label = platform || "#{RbConfig::CONFIG['host_os']}/#{RbConfig::CONFIG['host_cpu']}"
|
|
213
|
+
return warn_nil(warn, "no ffmpeg build for this platform (#{label})")
|
|
214
|
+
end
|
|
215
|
+
return warn_nil(warn, 'ffmpeg build not published yet') unless asset[:sha256]
|
|
216
|
+
return warn_nil(warn, "refusing non-https ffmpeg url: #{asset[:url]}") unless https?(asset[:url])
|
|
217
|
+
|
|
218
|
+
download(cached_path(env:, manifest:, platform:), asset, fetch, warn)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def download(destination, asset, fetch, warn)
|
|
222
|
+
part = "#{destination}.#{Process.pid}.#{SecureRandom.hex(4)}.part"
|
|
223
|
+
created = missing_dirs(File.dirname(destination))
|
|
224
|
+
begin
|
|
225
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
226
|
+
fetch.call(asset[:url], part)
|
|
227
|
+
return warn_nil(warn, "ffmpeg checksum mismatch for #{asset[:url]}; download discarded") unless verified?(part, asset[:sha256])
|
|
228
|
+
|
|
229
|
+
File.chmod(0o755, part)
|
|
230
|
+
# Rename wins: if another process already installed a valid binary, keep it.
|
|
231
|
+
File.rename(part, destination) unless File.file?(destination) && verified?(destination, asset[:sha256])
|
|
232
|
+
@verified[verification_key(destination, asset[:sha256])] = true
|
|
233
|
+
destination
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
warn_nil(warn, "ffmpeg download failed: #{e.message}")
|
|
236
|
+
ensure
|
|
237
|
+
FileUtils.rm_f(part)
|
|
238
|
+
remove_empty(created)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# The dirs mkdir_p would create for dir, deepest first.
|
|
243
|
+
def missing_dirs(dir)
|
|
244
|
+
missing = []
|
|
245
|
+
until File.exist?(dir) || File.dirname(dir) == dir
|
|
246
|
+
missing << dir
|
|
247
|
+
dir = File.dirname(dir)
|
|
248
|
+
end
|
|
249
|
+
missing
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# After a failed download, so CI never caches an empty ffmpeg/<version>/.
|
|
253
|
+
# Dir.rmdir only removes empty dirs, so a concurrent install is kept.
|
|
254
|
+
def remove_empty(dirs)
|
|
255
|
+
dirs.each { |dir| Dir.rmdir(dir) }
|
|
256
|
+
rescue SystemCallError
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def verified?(path, sha256)
|
|
261
|
+
Digest::SHA256.file(path).hexdigest.casecmp?(sha256)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def executable_file?(path)
|
|
265
|
+
File.file?(path) && File.executable?(path)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def https?(url)
|
|
269
|
+
URI(url).scheme == 'https'
|
|
270
|
+
rescue URI::InvalidURIError
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def warn_nil(warn, message)
|
|
275
|
+
warn.call(message)
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
data/lib/ruby_raider.rb
CHANGED
|
@@ -8,6 +8,8 @@ autoload :UtilityCommands, File.expand_path('commands/utility_commands', __dir__
|
|
|
8
8
|
|
|
9
9
|
module RubyRaider
|
|
10
10
|
class Raider < Thor
|
|
11
|
+
def self.exit_on_failure? = true
|
|
12
|
+
|
|
11
13
|
no_tasks do
|
|
12
14
|
def current_version = File.read(File.expand_path('version', __dir__)).strip
|
|
13
15
|
end
|
|
@@ -52,9 +54,6 @@ module RubyRaider
|
|
|
52
54
|
no_tasks do
|
|
53
55
|
def generate_from_parameters(params)
|
|
54
56
|
InvokeGenerators.generate_framework(params)
|
|
55
|
-
rescue InvokeGenerators::InvalidParameters => e
|
|
56
|
-
puts "Invalid parameters: #{e.message}"
|
|
57
|
-
exit 1
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
def merge_flags(params)
|
|
@@ -66,9 +66,7 @@ module ScaffoldProjectDetector
|
|
|
66
66
|
def validate_project
|
|
67
67
|
warnings = []
|
|
68
68
|
warnings << 'Gemfile not found. Are you in a Ruby Raider project directory?' unless File.exist?('Gemfile')
|
|
69
|
-
unless File.exist?(CONFIG_PATH)
|
|
70
|
-
warnings << "#{CONFIG_PATH} not found. Scaffolded pages won't include URL navigation methods."
|
|
71
|
-
end
|
|
69
|
+
warnings << "#{CONFIG_PATH} not found. Scaffolded pages won't include URL navigation methods." unless File.exist?(CONFIG_PATH)
|
|
72
70
|
warnings
|
|
73
71
|
end
|
|
74
72
|
|
data/lib/utilities/utilities.rb
CHANGED
|
@@ -7,7 +7,12 @@ module Utilities
|
|
|
7
7
|
|
|
8
8
|
class << self
|
|
9
9
|
def browser=(browser)
|
|
10
|
-
|
|
10
|
+
reload_config
|
|
11
|
+
name = browser.to_s.delete_prefix(':')
|
|
12
|
+
@config['browser'] = name
|
|
13
|
+
@config['browser_arguments'] ||= {}
|
|
14
|
+
@config['browser_arguments'][name] ||= []
|
|
15
|
+
overwrite_yaml
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def page_path=(path)
|
|
@@ -54,7 +59,8 @@ module Utilities
|
|
|
54
59
|
def delete_browser_options
|
|
55
60
|
reload_config
|
|
56
61
|
browser_name = @config['browser'] || 'chrome'
|
|
57
|
-
@config['browser_arguments']
|
|
62
|
+
@config['browser_arguments'] ||= {}
|
|
63
|
+
@config['browser_arguments'][browser_name] = []
|
|
58
64
|
overwrite_yaml
|
|
59
65
|
end
|
|
60
66
|
|
|
@@ -62,6 +68,13 @@ module Utilities
|
|
|
62
68
|
set('headless', enabled)
|
|
63
69
|
end
|
|
64
70
|
|
|
71
|
+
def video_enabled=(enabled)
|
|
72
|
+
reload_config
|
|
73
|
+
video = @config['video']
|
|
74
|
+
@config['video'] = (video.is_a?(Hash) ? video : {}).merge('enabled' => enabled)
|
|
75
|
+
overwrite_yaml
|
|
76
|
+
end
|
|
77
|
+
|
|
65
78
|
# Set multiple config keys in a single YAML write.
|
|
66
79
|
# Usage: Utilities.batch_update(browser: 'chrome', timeout: 30)
|
|
67
80
|
def batch_update(**settings)
|
data/lib/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.11.0
|
data/ruby_raider.gemspec
CHANGED
|
@@ -13,12 +13,15 @@ Gem::Specification.new do |s|
|
|
|
13
13
|
s.files = `git ls-files -z`.split("\x0")
|
|
14
14
|
s.bindir = 'bin'
|
|
15
15
|
s.executables << 'raider'
|
|
16
|
+
s.add_development_dependency 'bundler-audit', '~> 0.9'
|
|
16
17
|
s.add_development_dependency 'rake', '~> 13.0.6'
|
|
17
|
-
s.add_development_dependency 'reek', '~> 6.
|
|
18
|
+
s.add_development_dependency 'reek', '~> 6.5'
|
|
18
19
|
s.add_development_dependency 'rspec', '~> 3.11.0'
|
|
19
20
|
s.add_development_dependency 'rubocop', '~> 1.27'
|
|
20
21
|
s.add_development_dependency 'rubocop-performance', '~> 1.15.0'
|
|
21
22
|
s.add_development_dependency 'rubocop-rspec', '~> 2.9.0'
|
|
23
|
+
s.add_development_dependency 'ruby_audit', '~> 3.1'
|
|
24
|
+
s.add_development_dependency 'selenium-webdriver', '~> 4.0'
|
|
22
25
|
s.add_development_dependency 'steep', '~> 1.9'
|
|
23
26
|
|
|
24
27
|
s.add_runtime_dependency 'thor', '~> 1.2.1'
|
|
@@ -5,10 +5,13 @@ class UtilityCommands < Thor
|
|
|
5
5
|
def browser: (?String? default_browser) -> void
|
|
6
6
|
def browser_options: (*String opts) -> void
|
|
7
7
|
def raid: () -> void
|
|
8
|
-
def config: () -> void
|
|
9
8
|
def timeout: (String seconds) -> void
|
|
10
9
|
def viewport: (String dimensions) -> void
|
|
11
10
|
def platform: (String platform) -> void
|
|
12
11
|
def start_appium: () -> void
|
|
13
12
|
def desktop: () -> void
|
|
13
|
+
def video: (String action) -> void
|
|
14
|
+
def video_install: () -> void
|
|
15
|
+
def video_status: () -> void
|
|
16
|
+
def video_toggle: (bool enabled) -> void
|
|
14
17
|
end
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Type definitions for InvokeGenerators module
|
|
2
2
|
module InvokeGenerators
|
|
3
|
-
class InvalidParameters < ArgumentError
|
|
4
|
-
end
|
|
5
|
-
|
|
6
3
|
def self?.generate_framework: (?Hash[Symbol, untyped] structure) -> void
|
|
4
|
+
def self?.normalise_options: (Hash[Symbol, untyped] structure) -> Hash[Symbol, untyped]
|
|
5
|
+
def self?.validate_option: (Symbol key, untyped value, Array[String] allowed) -> void
|
|
7
6
|
def self?.validate_ci: (untyped provider) -> void
|
|
8
7
|
def self?.print_mobile_azure_notice: (Hash[Symbol, untyped] structure) -> void
|
|
9
8
|
def self?.add_generator: (Array[String] generators, *String gens) -> void
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module RubyRaider
|
|
2
|
+
module Ffmpeg
|
|
3
|
+
type asset = { url: String, sha256: String? }
|
|
4
|
+
type manifest = { version: String, assets: Hash[String, asset] }
|
|
5
|
+
|
|
6
|
+
MAX_REDIRECTS: Integer
|
|
7
|
+
DOWNLOAD_TIMEOUT: Integer
|
|
8
|
+
RELEASE_BASE: String
|
|
9
|
+
MANIFEST: manifest
|
|
10
|
+
PLATFORMS: Array[String]
|
|
11
|
+
DEFAULT_WARN: ^(String) -> void
|
|
12
|
+
|
|
13
|
+
self.@verified: Hash[Array[untyped], bool]
|
|
14
|
+
|
|
15
|
+
def self.platform: (?host_os: String, ?host_cpu: String) -> String?
|
|
16
|
+
def self.cache_dir: (?env: Hash[String, String], ?platform: String?) -> String
|
|
17
|
+
def self.cached_path: (?env: Hash[String, String], ?manifest: manifest, ?platform: String?) -> String
|
|
18
|
+
def self.resolve: (?env: Hash[String, String], ?manifest: manifest, ?warn: (^(String) -> void)?, ?platform: String?) -> String?
|
|
19
|
+
def self.ensure!: (?env: Hash[String, String], ?manifest: manifest, ?fetch: (^(String, String) -> untyped)?, ?warn: (^(String) -> void)?, ?platform: String?) -> String?
|
|
20
|
+
def self.obtainable?: (?env: Hash[String, String], ?manifest: manifest, ?platform: String?) -> bool
|
|
21
|
+
def self.http_get: (String url, String destination, ?Integer limit, ?timeout: Numeric, ?deadline: Float?) -> void
|
|
22
|
+
end
|
|
23
|
+
end
|
data/sig/utilities/utilities.rbs
CHANGED
|
@@ -13,6 +13,7 @@ module Utilities
|
|
|
13
13
|
def self?.platform=: (String platform) -> void
|
|
14
14
|
def self?.browser_options=: (Array[String] opts) -> void
|
|
15
15
|
def self?.delete_browser_options: () -> void
|
|
16
|
+
def self?.video_enabled=: (bool enabled) -> void
|
|
16
17
|
def self?.batch_update: (**untyped settings) -> void
|
|
17
18
|
def self?.run: (?String? opts) -> void
|
|
18
19
|
def self?.parallel_run: (?String? opts, ?untyped? _settings) -> void
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require 'rspec'
|
|
4
4
|
require_relative '../../lib/ruby_raider'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Command specs are grouped by CLI area under spec/commands, not by constant path
|
|
7
|
+
RSpec.describe RubyRaider::Raider do # rubocop:disable RSpec/FilePath
|
|
7
8
|
describe 'new command' do
|
|
8
9
|
context 'with parameters' do
|
|
9
10
|
it 'calls InvokeGenerators.generate_framework with parsed hash' do
|
|
@@ -65,4 +66,58 @@ RSpec.describe RubyRaider::Raider do
|
|
|
65
66
|
expect(subcommands).to include('utility')
|
|
66
67
|
end
|
|
67
68
|
end
|
|
69
|
+
|
|
70
|
+
describe 'CLI exit status' do
|
|
71
|
+
require 'open3'
|
|
72
|
+
require 'tmpdir'
|
|
73
|
+
|
|
74
|
+
let(:raider_bin) { File.expand_path('../../bin/raider', __dir__) }
|
|
75
|
+
|
|
76
|
+
def run_raider(*args, chdir: Dir.pwd)
|
|
77
|
+
Open3.capture3(RbConfig.ruby, raider_bin, *args, chdir:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'exits non-zero for an unknown command' do
|
|
81
|
+
_out, err, status = run_raider('u', 'debug', 'on')
|
|
82
|
+
|
|
83
|
+
expect(status.exitstatus).not_to eq(0)
|
|
84
|
+
expect(err).to include('Could not find command "debug"')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'exits zero for version' do
|
|
88
|
+
_out, _err, status = run_raider('version')
|
|
89
|
+
|
|
90
|
+
expect(status.exitstatus).to eq(0)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'rejects an unknown framework without creating the project' do
|
|
94
|
+
Dir.mktmpdir do |dir|
|
|
95
|
+
out, err, status = run_raider('new', 'x', '-p', 'framework:minitest', 'automation:selenium', chdir: dir)
|
|
96
|
+
|
|
97
|
+
expect(status.exitstatus).not_to eq(0)
|
|
98
|
+
expect(out + err).to include('framework', 'rspec', 'cucumber')
|
|
99
|
+
expect(File.exist?(File.join(dir, 'x'))).to be(false)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'rejects an unknown automation without creating the project' do
|
|
104
|
+
Dir.mktmpdir do |dir|
|
|
105
|
+
out, err, status = run_raider('new', 'x', '-p', 'framework:rspec', 'automation:playwright', chdir: dir)
|
|
106
|
+
|
|
107
|
+
expect(status.exitstatus).not_to eq(0)
|
|
108
|
+
expect(out + err).to include('automation', 'selenium', 'cross_platform')
|
|
109
|
+
expect(File.exist?(File.join(dir, 'x'))).to be(false)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'rejects automation:appium and points to the mobile platforms' do
|
|
114
|
+
Dir.mktmpdir do |dir|
|
|
115
|
+
out, err, status = run_raider('new', 'x', '-p', 'framework:rspec', 'automation:appium', chdir: dir)
|
|
116
|
+
|
|
117
|
+
expect(status.exitstatus).not_to eq(0)
|
|
118
|
+
expect(out + err).to include('automation', 'ios', 'android', 'cross_platform')
|
|
119
|
+
expect(File.exist?(File.join(dir, 'x'))).to be(false)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
68
123
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minimal stand-in for selenium-webdriver, which is not in ruby_raider's bundle.
|
|
4
|
+
# spec/generators/video_helper_spec.rb puts this directory on $LOAD_PATH so the
|
|
5
|
+
# rendered video_helper.rb can `require 'selenium-webdriver'`.
|
|
6
|
+
module Selenium
|
|
7
|
+
module WebDriver
|
|
8
|
+
module Error
|
|
9
|
+
class WebDriverError < StandardError; end
|
|
10
|
+
class NoSuchAlertError < WebDriverError; end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Support
|
|
14
|
+
class AbstractEventListener
|
|
15
|
+
def before_navigate_to(url, driver) end
|
|
16
|
+
def after_navigate_to(url, driver) end
|
|
17
|
+
def before_find(by, what, driver) end
|
|
18
|
+
def after_find(by, what, driver) end
|
|
19
|
+
def before_click(element, driver) end
|
|
20
|
+
def after_click(element, driver) end
|
|
21
|
+
def before_change_value_of(element, driver) end
|
|
22
|
+
def after_change_value_of(element, driver) end
|
|
23
|
+
def before_execute_script(script, driver) end
|
|
24
|
+
def after_execute_script(script, driver) end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|