metasploit-framework/lib/metasploit/framework/spec/constants/suite.rb

119 lines
4.0 KiB
Ruby

# Logs if constants created by module loading are left over after suite has completed.
module Metasploit::Framework::Spec::Constants::Suite
#
# CONSTANTS
#
LOGS_PATHNAME = Pathname.new('log/metasploit/framework/spec/constants/suite')
# Logs leaked constants to {LOG_PATHNAME} and prints `message` to stderr.
#
# @param hook (see log_pathname)
# @param message [String] additional message printed to stderr when there is at least one leaked constant.
# @return [void]
def self.log_leaked_constants(hook, message)
count = 0
hook_log_pathname = log_pathname(hook)
hook_log_pathname.parent.mkpath
hook_log_pathname.open('w') do |f|
count = Metasploit::Framework::Spec::Constants.each do |child_name|
f.puts child_name
end
end
if count > 0
$stderr.puts "#{count} #{'constant'.pluralize(count)} leaked under " \
"#{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT}. #{message} See #{hook_log_pathname} " \
"for details."
else
hook_log_pathname.delete
end
end
# Configures after(:suite) callback for RSpec to check for leaked constants.
def self.configure!
unless @configured
RSpec.configure do |config|
config.before(:suite) do
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
:before,
'Modules are being loaded outside callbacks before suite starts.'
)
end
config.after(:suite) do
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
:after,
'Modules are being loaded inside callbacks or examples during suite run.'
)
end
end
@configured = true
end
end
# Adds action to `spec` task so that `rake spec` fails if `log/leaked-constants.log` exists after printing out the
# leaked constants.
#
# @return [void]
def self.define_task
Rake::Task.define_task(:spec) do
leaked_before = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:before)
leaked_after = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:after)
# leaks after suite can be be cleaned up by {Metasploit::Framework::Spec::Constants::Each.configure!}, but
# leaks before suite require user intervention to find the leaks since it's a programming error in how the specs
# are written where Modules are being loaded in the context scope.
if leaked_after
$stderr.puts
$stderr.puts "Add `Metasploit::Framework::Spec::Constants::Each.configure!` to `spec/spec_helper.rb` " \
"**NOTE: `Metasploit::Framework::Spec::Constants::Each` may report false leaks if `after(:all)` " \
"is used to clean up constants instead of `after(:each)`**"
end
if leaked_before || leaked_after
exit 1
end
end
end
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
# suite runs.
def self.log_pathname(hook)
LOGS_PATHNAME.join("#{hook}.log")
end
# Prints logged leaked constants to stderr.
#
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
# suite runs.
# @return [true] if leaks printed
# @return [false] otherwise
def self.print_leaked_constants(hook)
hook_log_pathname = log_pathname(hook)
leaks = false
if hook_log_pathname.exist?
leaks = true
$stderr.puts "Leaked constants detected under #{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT} #{hook} suite:"
hook_log_pathname.open do |f|
f.each_line do |line|
constant_name = line.strip
full_name = Metasploit::Framework::Spec::Constants.full_name(constant_name)
if full_name
formatted_full_name = " # #{full_name}"
end
$stderr.puts " #{constant_name}#{formatted_full_name}"
end
end
end
leaks
end
end