rails-profiler 0.30.9 → 0.30.11
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/CHANGELOG.md +107 -0
- data/app/controllers/profiler/api/cluster_controller.rb +37 -1
- data/app/controllers/profiler/api/env_vars_controller.rb +6 -0
- data/app/controllers/profiler/api/slave_proxy_controller.rb +4 -2
- data/app/controllers/profiler/api/test_runner_controller.rb +3 -9
- data/app/controllers/profiler/application_controller.rb +12 -1
- data/app/controllers/profiler/profiles_controller.rb +2 -0
- data/config/routes.rb +15 -8
- data/lib/profiler/boot_env.rb +8 -0
- data/lib/profiler/cluster/master_client.rb +24 -6
- data/lib/profiler/cluster/security.rb +179 -0
- data/lib/profiler/cluster/slave_proxy.rb +24 -1
- data/lib/profiler/collectors/base_collector.rb +2 -0
- data/lib/profiler/collectors/console_collector.rb +2 -2
- data/lib/profiler/collectors/flamegraph_collector.rb +2 -2
- data/lib/profiler/collectors/i18n_collector.rb +1 -1
- data/lib/profiler/collectors/mailer_collector.rb +5 -5
- data/lib/profiler/configuration.rb +25 -2
- data/lib/profiler/console_profiler.rb +1 -1
- data/lib/profiler/env_override_store.rb +11 -0
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +2 -1
- data/lib/profiler/job_profiler.rb +2 -2
- data/lib/profiler/mcp/http_guard.rb +73 -0
- data/lib/profiler/mcp/server.rb +5 -2
- data/lib/profiler/mcp/tools/delete_env_var.rb +7 -0
- data/lib/profiler/mcp/tools/run_tests.rb +7 -1
- data/lib/profiler/mcp/tools/set_env_var.rb +7 -0
- data/lib/profiler/models/profile.rb +14 -4
- data/lib/profiler/railtie.rb +5 -1
- data/lib/profiler/redaction.rb +98 -0
- data/lib/profiler/test_runner/discovery.rb +2 -2
- data/lib/profiler/test_runner/run_store.rb +39 -2
- data/lib/profiler/test_runner/runner.rb +144 -10
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +2 -0
- metadata +6 -3
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "../boot_env"
|
|
5
|
+
require_relative "../env_override_store"
|
|
3
6
|
require_relative "run_store"
|
|
4
7
|
require_relative "discovery"
|
|
5
8
|
|
|
6
9
|
module Profiler
|
|
7
10
|
module TestRunner
|
|
11
|
+
# Raised by Runner.start when the selection holds a file that is not a discovered test.
|
|
12
|
+
class InvalidFileError < ArgumentError; end
|
|
13
|
+
|
|
8
14
|
class Runner
|
|
15
|
+
# "spec/models/user_spec.rb:12" or ":12:30": the line selection rspec and minitest accept.
|
|
16
|
+
LINE_SUFFIX = /\A(.+?)((?::\d+)+)\z/
|
|
17
|
+
|
|
18
|
+
@warn_mutex = Mutex.new
|
|
19
|
+
@warned = {}
|
|
20
|
+
|
|
21
|
+
# Every caller (the HTTP controller, the MCP run_tests tool) goes through here, so the
|
|
22
|
+
# selection is checked once, before any run is recorded or any process is started.
|
|
9
23
|
def self.start(files:, framework:)
|
|
24
|
+
validate_files!(files, framework)
|
|
10
25
|
run = Profiler::TestRunner.run_store.create(files: files, framework: framework)
|
|
11
26
|
spawn_async(run)
|
|
12
27
|
run
|
|
@@ -26,6 +41,77 @@ module Profiler
|
|
|
26
41
|
end
|
|
27
42
|
end
|
|
28
43
|
|
|
44
|
+
# Accepts only files listed by Discovery.files for the framework, compared by their
|
|
45
|
+
# canonical path (File.realpath, symbolic links resolved), so that a test run cannot be
|
|
46
|
+
# used to execute any other Ruby file of the application.
|
|
47
|
+
def self.validate_files!(files, framework)
|
|
48
|
+
files = Array(files)
|
|
49
|
+
raise InvalidFileError, "No files selected" if files.empty?
|
|
50
|
+
|
|
51
|
+
root = project_root
|
|
52
|
+
allowed = if Profiler.configuration.test_runner_allow_undiscovered_files
|
|
53
|
+
warn_once(:undiscovered,
|
|
54
|
+
"[Profiler] config.test_runner_allow_undiscovered_files is enabled: the test runner " \
|
|
55
|
+
"runs any file under the Rails root, not only the discovered tests.")
|
|
56
|
+
nil
|
|
57
|
+
else
|
|
58
|
+
discovered_real_paths(root, framework)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
refused = files.reject do |entry|
|
|
62
|
+
next false unless entry.is_a?(String) && !entry.empty? && !entry.include?("\0")
|
|
63
|
+
|
|
64
|
+
path = entry[LINE_SUFFIX, 1] || entry
|
|
65
|
+
allowed ? allowed.include?(real_path(File.join(root, path))) : under_root?(root, File.join(root, path))
|
|
66
|
+
end
|
|
67
|
+
return if refused.empty?
|
|
68
|
+
|
|
69
|
+
message = allowed ? "Not a discovered test file" : "Not a file under the Rails root"
|
|
70
|
+
raise InvalidFileError, "#{message}: #{refused.map(&:to_s).join(", ")}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.discovered_real_paths(root, framework)
|
|
74
|
+
real_root = real_path(root)
|
|
75
|
+
Discovery.files(framework: framework).flat_map { |dir| dir[:files] }.filter_map do |file|
|
|
76
|
+
path = real_path(File.join(root, file[:path]))
|
|
77
|
+
# A discovered symbolic link whose target leaves the project is not a project test.
|
|
78
|
+
path if path && real_root && path.start_with?("#{real_root}/")
|
|
79
|
+
end.to_set
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.real_path(path)
|
|
83
|
+
File.realpath(path)
|
|
84
|
+
rescue SystemCallError, ArgumentError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.under_root?(root, path)
|
|
89
|
+
expanded = File.expand_path(path)
|
|
90
|
+
expanded == root || expanded.start_with?("#{root}/")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.project_root
|
|
94
|
+
defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.warn_once(key, message)
|
|
98
|
+
@warn_mutex.synchronize do
|
|
99
|
+
return if @warned[key]
|
|
100
|
+
|
|
101
|
+
@warned[key] = true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
105
|
+
Rails.logger.warn(message)
|
|
106
|
+
else
|
|
107
|
+
warn(message)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.reset_warnings!
|
|
112
|
+
@warn_mutex.synchronize { @warned = {} }
|
|
113
|
+
end
|
|
114
|
+
|
|
29
115
|
private
|
|
30
116
|
|
|
31
117
|
def self.spawn_async(run)
|
|
@@ -38,6 +124,7 @@ module Profiler
|
|
|
38
124
|
finished_at: Time.now
|
|
39
125
|
)
|
|
40
126
|
Profiler::TestRunner.run_store.append_output(run.id, "\n[Profiler] Error: #{e.message}\n")
|
|
127
|
+
Profiler::TestRunner.run_store.finish_output(run.id)
|
|
41
128
|
end
|
|
42
129
|
end
|
|
43
130
|
|
|
@@ -47,13 +134,19 @@ module Profiler
|
|
|
47
134
|
|
|
48
135
|
Profiler::TestRunner.run_store.update(run.id, status: "running", started_at: Time.now)
|
|
49
136
|
|
|
50
|
-
|
|
137
|
+
# unsetenv_others: the child gets exactly env, not the ENV of this process
|
|
138
|
+
IO.popen([env, *cmd, unsetenv_others: true, err: [:child, :out]], "r") do |io|
|
|
51
139
|
Profiler::TestRunner.run_store.update(run.id, pid: io.pid)
|
|
52
140
|
|
|
53
|
-
|
|
54
|
-
|
|
141
|
+
begin
|
|
142
|
+
while (chunk = io.read(256))
|
|
143
|
+
break if chunk.empty?
|
|
55
144
|
|
|
56
|
-
|
|
145
|
+
Profiler::TestRunner.run_store.append_output(run.id, chunk)
|
|
146
|
+
end
|
|
147
|
+
ensure
|
|
148
|
+
# The process printed everything, killed or not: what was held back can be shown.
|
|
149
|
+
Profiler::TestRunner.run_store.finish_output(run.id)
|
|
57
150
|
end
|
|
58
151
|
end
|
|
59
152
|
|
|
@@ -69,7 +162,7 @@ module Profiler
|
|
|
69
162
|
end
|
|
70
163
|
|
|
71
164
|
def self.build_command(files, framework)
|
|
72
|
-
root =
|
|
165
|
+
root = project_root
|
|
73
166
|
absolute_files = files.map { |f| File.join(root, f) }
|
|
74
167
|
|
|
75
168
|
case framework.to_sym
|
|
@@ -82,22 +175,63 @@ module Profiler
|
|
|
82
175
|
end
|
|
83
176
|
end
|
|
84
177
|
|
|
85
|
-
BLOCKED_ENV_KEYS = %w[RAILS_ENV RACK_ENV DATABASE_URL SECRET_KEY_BASE].freeze
|
|
178
|
+
BLOCKED_ENV_KEYS = %w[RAILS_ENV RACK_ENV DATABASE_URL SECRET_KEY_BASE PROFILER_TEST_RUNNER_CHILD].freeze
|
|
179
|
+
|
|
180
|
+
# Overrides of these variables make the test process, or the shell shims (rbenv, asdf) that
|
|
181
|
+
# start it, load or run code other than the selected tests. It is a deny list, so it cannot
|
|
182
|
+
# be complete; a variable needed by the tests can be set in the shell that starts Rails.
|
|
183
|
+
CODE_LOADING_ENV_KEYS = %w[
|
|
184
|
+
SPEC_OPTS TESTOPTS TEST PATH HOME XDG_CONFIG_HOME GEMRC NODE_OPTIONS NODE_PATH
|
|
185
|
+
SHELLOPTS BASHOPTS PS4 ENV CDPATH IFS
|
|
186
|
+
].freeze
|
|
187
|
+
CODE_LOADING_ENV_PREFIXES = %w[
|
|
188
|
+
RUBY GEM_ BUNDLE_ BUNDLER_ LD_ DYLD_ BASH_ RBENV_ ASDF_ RVM_ CHRUBY GIT_ BOOTSNAP_
|
|
189
|
+
PYTHON PERL5
|
|
190
|
+
].freeze
|
|
191
|
+
ENV_NAME = /\A[A-Z_][A-Z0-9_]*\z/i
|
|
192
|
+
|
|
193
|
+
def self.code_loading_env_key?(key)
|
|
194
|
+
name = key.to_s.upcase
|
|
195
|
+
return true unless ENV_NAME.match?(name)
|
|
196
|
+
|
|
197
|
+
CODE_LOADING_ENV_KEYS.include?(name) || CODE_LOADING_ENV_PREFIXES.any? { |prefix| name.start_with?(prefix) }
|
|
198
|
+
end
|
|
86
199
|
|
|
200
|
+
# Starts from the copy of ENV taken when the gem was loaded, not from ENV: the overrides are
|
|
201
|
+
# also written into ENV (by the env vars endpoint, the MCP tools and EnvOverrideStore#apply!),
|
|
202
|
+
# and one whose entry the store lost would pass unnoticed. Only the overrides read from the
|
|
203
|
+
# store and admitted are applied on top; a left-out or blocked key keeps its shell value.
|
|
87
204
|
def self.build_env
|
|
88
|
-
base =
|
|
205
|
+
base = Profiler::BOOT_ENV.dup
|
|
206
|
+
left_out = []
|
|
89
207
|
|
|
90
|
-
# Inject env var overrides configured in the profiler — skip blocked keys
|
|
91
208
|
overrides = Profiler.env_override_store.all_overrides
|
|
92
209
|
overrides.each do |key, entry|
|
|
93
|
-
|
|
210
|
+
blocked = BLOCKED_ENV_KEYS.include?(key.upcase)
|
|
211
|
+
if blocked || code_loading_env_key?(key)
|
|
212
|
+
left_out << key unless blocked
|
|
213
|
+
next
|
|
214
|
+
end
|
|
94
215
|
value = entry.is_a?(Hash) ? entry["value"] : entry
|
|
95
|
-
|
|
216
|
+
if value == EnvOverrideStore::DELETED_SENTINEL
|
|
217
|
+
base.delete(key)
|
|
218
|
+
else
|
|
219
|
+
base[key] = value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
unless left_out.empty?
|
|
224
|
+
warn_once([:env, left_out.sort],
|
|
225
|
+
"[Profiler] Environment overrides not passed to the test runner, because they make it " \
|
|
226
|
+
"load other code: #{left_out.sort.join(", ")}.")
|
|
96
227
|
end
|
|
97
228
|
|
|
98
229
|
# Ensure test environment regardless of overrides
|
|
99
230
|
base["RAILS_ENV"] = "test"
|
|
100
231
|
base["RACK_ENV"] = "test"
|
|
232
|
+
# The test process boots the same application: its EnvOverrideStore#apply! must not
|
|
233
|
+
# replay the overrides left out above
|
|
234
|
+
base[EnvOverrideStore::TEST_RUNNER_CHILD_ENV] = "1"
|
|
101
235
|
|
|
102
236
|
base
|
|
103
237
|
end
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.30.
|
|
4
|
+
version: 0.30.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
61
|
+
version: '0.3'
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
68
|
+
version: '0.3'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: webmock
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -133,7 +133,9 @@ files:
|
|
|
133
133
|
- config/routes.rb
|
|
134
134
|
- exe/profiler-mcp
|
|
135
135
|
- lib/profiler.rb
|
|
136
|
+
- lib/profiler/boot_env.rb
|
|
136
137
|
- lib/profiler/cluster/master_client.rb
|
|
138
|
+
- lib/profiler/cluster/security.rb
|
|
137
139
|
- lib/profiler/cluster/slave_proxy.rb
|
|
138
140
|
- lib/profiler/cluster/slave_registry.rb
|
|
139
141
|
- lib/profiler/cluster/token_cache.rb
|
|
@@ -171,6 +173,7 @@ files:
|
|
|
171
173
|
- lib/profiler/local_request.rb
|
|
172
174
|
- lib/profiler/mcp/body_formatter.rb
|
|
173
175
|
- lib/profiler/mcp/file_cache.rb
|
|
176
|
+
- lib/profiler/mcp/http_guard.rb
|
|
174
177
|
- lib/profiler/mcp/path_extractor.rb
|
|
175
178
|
- lib/profiler/mcp/resources/failing_tests.rb
|
|
176
179
|
- lib/profiler/mcp/resources/n1_patterns.rb
|