lib/raap/cli.rb in raap-0.3.0 vs lib/raap/cli.rb in raap-0.4.0
- old
+ new
@@ -1,265 +1,335 @@
# frozen_string_literal: true
module RaaP
# $ raap Integer#pow
- # $ raap -I sig RaaP::Type
class CLI
- class << self
- attr_accessor :option
- end
-
Option = Struct.new(
:dirs,
:requires,
:libraries,
:timeout,
:size_from,
:size_to,
:size_by,
:allow_private,
+ :skips,
keyword_init: true
)
- # defaults
- self.option = Option.new(
- dirs: [],
- requires: [],
- libraries: [],
- timeout: 3,
- size_from: 0,
- size_to: 99,
- size_by: 1,
- allow_private: false,
- )
+ # Should skip methods has side effects
+ DEFAULT_SKIP = Set.new
+ %i[
+ fork system exec spawn `
+ abort exit exit! raise fail
+ load require require_relative
+ gem
+ ].each do |kernel_method|
+ DEFAULT_SKIP << "::Kernel##{kernel_method}"
+ DEFAULT_SKIP << "::Kernel.#{kernel_method}"
+ end
+ %i[
+ delete unlink chmod lchmod chown lchown
+ link mkfifo new open rename truncate
+ ].each { |m| DEFAULT_SKIP << "::File.#{m}" }
+ %i[flock truncate].each { |m| DEFAULT_SKIP << "::File##{m}" }
+ attr_accessor :option, :argv, :skip, :results
+
def initialize(argv)
+ # defaults
+ @option = Option.new(
+ dirs: [],
+ requires: [],
+ libraries: [],
+ timeout: 3,
+ size_from: 0,
+ size_to: 99,
+ size_by: 1,
+ skips: [],
+ allow_private: false,
+ )
@argv = argv
+ @skip = DEFAULT_SKIP.dup
+ @results = []
end
def load
OptionParser.new do |o|
o.on('-I', '--include PATH') do |path|
- CLI.option.dirs << path
+ @option.dirs << path
end
o.on('--library lib', 'load rbs library') do |lib|
- CLI.option.libraries << lib
+ @option.libraries << lib
end
o.on('--require lib', 'require ruby library') do |lib|
- CLI.option.requires << lib
+ @option.requires << lib
end
o.on('--log-level level', "default: warn") do |arg|
RaaP.logger.level = arg
end
- o.on('--timeout sec', Integer, "default: #{CLI.option.timeout}") do |arg|
- CLI.option.timeout = arg
+ o.on('--timeout sec', Float, "default: #{@option.timeout}") do |arg|
+ @option.timeout = arg
end
- o.on('--size-from int', Integer, "default: #{CLI.option.size_from}") do |arg|
- CLI.option.size_from = arg
+ o.on('--size-from int', Integer, "default: #{@option.size_from}") do |arg|
+ @option.size_from = arg
end
- o.on('--size-to int', Integer, "default: #{CLI.option.size_to}") do |arg|
- CLI.option.size_to = arg
+ o.on('--size-to int', Integer, "default: #{@option.size_to}") do |arg|
+ @option.size_to = arg
end
- o.on('--size-by int', Integer, "default: #{CLI.option.size_by}") do |arg|
- CLI.option.size_by = arg
+ o.on('--size-by int', Integer, "default: #{@option.size_by}") do |arg|
+ @option.size_by = arg
end
- o.on('--allow-private', "default: #{CLI.option.allow_private}") do
- CLI.option.allow_private = true
+ o.on('--allow-private', "default: #{@option.allow_private}") do
+ @option.allow_private = true
end
+ o.on('--skip tag', "skip case (e.g. `Foo#meth`)") do |tag|
+ @option.skips << tag
+ end
end.parse!(@argv)
- CLI.option.dirs.each do |dir|
+ @option.dirs.each do |dir|
RaaP::RBS.loader.add(path: Pathname(dir))
end
- CLI.option.libraries.each do |lib|
+ @option.libraries.each do |lib|
RaaP::RBS.loader.add(library: lib, version: nil)
end
- CLI.option.requires.each do |lib|
+ @option.requires.each do |lib|
require lib
end
+ @option.skips.each do |skip|
+ @skip << skip
+ end
+ @skip.freeze
self
end
def run
+ Signal.trap(:INT) do
+ puts "Interrupted by SIGINT"
+ report
+ exit 1
+ end
+
@argv.map do |tag|
case
when tag.include?('#')
- run_by_instance(tag:)
+ run_by(kind: :instance, tag:)
when tag.include?('.')
- run_by_singleton(tag:)
+ run_by(kind: :singleton, tag:)
when tag.end_with?('*')
run_by_type_name_with_search(tag:)
else
run_by_type_name(tag:)
end
- end.each do |ret|
- ret.each do |methods|
- methods.each do |status, method_name, method_type|
- if status == 1
- puts "Fail:"
- puts "def #{method_name}: #{method_type}"
- end
- end
- end
end
- self
+ report
end
- def run_by_instance(tag:)
- t, m = tag.split('#', 2)
- t or raise
- m or raise
- type = RBS.parse_type(t)
- type = __skip__ = type
- raise "cannot specified #{type}" unless type.respond_to?(:name)
- receiver_type = Type.new(type.to_s)
- method_name = m.to_sym
- definition = RBS.builder.build_instance(type.name)
- type_params_decl = definition.type_params_decl
- method = definition.methods[method_name]
- raise "`#{tag}` is not found" unless method
- puts "# #{type.to_s}"
- puts
- [
- method.method_types.map do |method_type|
- property(receiver_type:, type_params_decl:, method_type:, method_name:)
+ private
+
+ def report
+ i = 0
+ exit_status = 0
+ @results.each do |ret|
+ ret => { method:, properties: }
+ properties.select { |status,| status == 1 }.each do |_, method_name, method_type, reason|
+ i += 1
+ location = if method.alias_of
+ alias_decl = RBS.find_alias_decl(method.defined_in, method_name) or raise "alias decl not found: #{method_name}"
+ alias_decl.location
+ else
+ method_type.location
+ end
+ prefix = method.defs.first.member.kind == :instance ? '' : 'self.'
+
+ puts "\e[41m\e[1m#\e[m\e[1m #{i}) Failure:\e[m"
+ puts
+ puts "def #{prefix}#{method_name}: #{method_type}"
+ puts " in #{location}"
+ puts
+ puts "## Reason"
+ puts
+ puts reason&.string
+ puts
+ exit_status = 1
end
- ]
+ end
+ exit_status
end
- def run_by_singleton(tag:)
- t, m = tag.split('.', 2)
+ def run_by(kind:, tag:)
+ split = kind == :instance ? '#' : '.'
+ t, m = tag.split(split, 2)
t or raise
m or raise
type = RBS.parse_type(t)
raise "cannot specified #{type.class}" unless type.respond_to?(:name)
+
type = __skip__ = type
- receiver_type = Type.new("singleton(#{type.name})")
+ type_name = type.name.absolute!
+ type_to_s = type.to_s.start_with?('::') ? type.to_s : "::#{type}"
+ receiver_type = if kind == :instance
+ Type.new(type_to_s)
+ else
+ Type.new("singleton(#{type_name})")
+ end
method_name = m.to_sym
- definition = RBS.builder.build_singleton(type.name)
+ definition = if kind == :instance
+ RBS.builder.build_instance(type_name)
+ else
+ RBS.builder.build_singleton(type_name)
+ end
+
method = definition.methods[method_name]
- type_params_decl = definition.type_params_decl
raise "`#{tag}` not found" unless method
- puts "# #{type}"
- puts
- [
- method.method_types.map do |method_type|
- property(receiver_type:, type_params_decl:, method_type:, method_name:)
+
+ if @skip.include?("#{type_name}#{split}#{method_name}")
+ raise "`#{type_name}#{split}#{method_name}` is a method to be skipped"
+ end
+
+ type_params_decl = definition.type_params_decl
+ type_args = type.args
+
+ RaaP.logger.info("# #{type}")
+ @results << {
+ method:,
+ properties: method.method_types.map do |method_type|
+ property(receiver_type:, type_params_decl:, type_args:, method_type:, method_name:)
end
- ]
+ }
end
def run_by_type_name_with_search(tag:)
first, _last = tag.split('::')
- ret = []
- RBS.env.class_decls.each do |name, entry|
+ RBS.env.class_decls.each do |name, _entry|
if ['', '::'].any? { |pre| name.to_s.match?(/\A#{pre}#{first}\b/) }
- ret << run_by_type_name(tag: name.to_s)
+ run_by_type_name(tag: name.to_s)
end
end
- ret.flatten(1)
end
def run_by_type_name(tag:)
type = RBS.parse_type(tag)
type = __skip__ = type
raise "cannot specified #{type.class}" unless type.respond_to?(:name)
+
type_name = type.name.absolute!
+ type_args = type.args
- ret = []
-
definition = RBS.builder.build_singleton(type_name)
type_params_decl = definition.type_params_decl
definition.methods.filter_map do |method_name, method|
+ next if @skip.include?("#{type_name.absolute!}.#{method_name}")
next unless method.accessibility == :public
next if method.defined_in != type_name
- next if method_name == :fork || method_name == :spawn # TODO: skip solution
- puts "# #{type_name}.#{method_name}"
- puts
- ret << method.method_types.map do |method_type|
- property(receiver_type: Type.new("singleton(#{type})"), type_params_decl:, method_type:, method_name:)
- end
+
+ RaaP.logger.info("# #{type_name}.#{method_name}")
+ @results << {
+ method:,
+ properties: method.method_types.map do |method_type|
+ property(receiver_type: Type.new("singleton(#{type.name})"), type_params_decl:, type_args:, method_type:, method_name:)
+ end
+ }
end
definition = RBS.builder.build_instance(type_name)
type_params_decl = definition.type_params_decl
definition.methods.filter_map do |method_name, method|
+ next if @skip.include?("#{type_name.absolute!}##{method_name}")
next unless method.accessibility == :public
next if method.defined_in != type_name
- next if method_name == :fork || method_name == :spawn # TODO: skip solution
- puts "# #{type_name}##{method_name}"
- puts
- ret << method.method_types.map do |method_type|
- property(receiver_type: Type.new(type.to_s), type_params_decl:, method_type:, method_name:)
- end
- end
- ret
+ RaaP.logger.info("# #{type_name}##{method_name}")
+ @results << {
+ method:,
+ properties: method.method_types.map do |method_type|
+ property(receiver_type: Type.new(type.name), type_params_decl:, type_args:, method_type:, method_name:)
+ end
+ }
+ end
end
- def property(receiver_type:, type_params_decl:, method_type:, method_name:)
+ def property(receiver_type:, type_params_decl:, type_args:, method_type:, method_name:)
rtype = __skip__ = receiver_type.type
if receiver_type.type.instance_of?(::RBS::Types::ClassSingleton)
prefix = 'self.'
- type_args = []
else
prefix = ''
- type_args = rtype.args
end
- puts "## def #{prefix}#{method_name}: #{method_type}"
+ type_params_decl.each_with_index do |_, i|
+ if rtype.instance_of?(::RBS::Types::ClassInstance)
+ rtype.args[i] = type_args[i] || ::RBS::Types::Bases::Any.new(location: nil)
+ end
+ end
+ RaaP.logger.info("## def #{prefix}#{method_name}: #{method_type}")
status = 0
- stats = MethodProperty.new(
+ reason = nil
+ prop = MethodProperty.new(
receiver_type:,
- method_name: method_name,
+ method_name:,
method_type: MethodType.new(
method_type,
type_params_decl:,
type_args:,
self_type: rtype,
instance_type: ::RBS::Types::ClassInstance.new(name: rtype.name, args: type_args, location: nil),
class_type: ::RBS::Types::ClassSingleton.new(name: rtype.name, location: nil),
),
- size_step: CLI.option.size_from.step(to: CLI.option.size_to, by: CLI.option.size_by),
- timeout: CLI.option.timeout,
+ size_step: @option.size_from.step(to: @option.size_to, by: @option.size_by),
+ timeout: @option.timeout,
allow_private: true,
- ).run do |called|
+ )
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ stats = prop.run do |called|
case called
in Result::Success => s
print '.'
RaaP.logger.debug { "Success: #{s.called_str}" }
in Result::Failure => f
puts 'F'
- puts "Failed in case of `#{f.called_str}`"
- if e = f.exception
+ if (e = f.exception)
RaaP.logger.debug { "Failure: [#{e.class}] #{e.message}" }
+ RaaP.logger.debug { e.backtrace.join("\n") }
end
- puts
RaaP.logger.debug { PP.pp(f.symbolic_call, ''.dup) }
- puts "### call stack:"
- puts
- puts "```"
- puts SymbolicCaller.new(f.symbolic_call).to_lines.join("\n")
- puts "```"
+ reason = StringIO.new
+ reason.puts "Failed in case of `#{f.called_str}`"
+ reason.puts
+ reason.puts "### Repro"
+ reason.puts
+ reason.puts "```rb"
+ reason.puts SymbolicCaller.new(f.symbolic_call).to_lines.join("\n")
+ reason.puts "```"
status = 1
throw :break
in Result::Skip => s
print 'S'
- RaaP.logger.debug { PP.pp(s.symbolic_call, ''.dup) }
- RaaP.logger.debug("Skip: [#{s.exception.class}] #{s.exception.message}")
+ RaaP.logger.debug { "\n```\n#{SymbolicCaller.new(s.symbolic_call).to_lines.join("\n")}\n```" }
+ RaaP.logger.debug("Skip: #{s.exception.detailed_message}")
RaaP.logger.debug(s.exception.backtrace.join("\n"))
in Result::Exception => e
print 'E'
- RaaP.logger.debug { PP.pp(e.symbolic_call, ''.dup) }
- RaaP.logger.debug("Exception: [#{e.exception.class}] #{e.exception.message}")
+ RaaP.logger.debug { "\n```\n#{SymbolicCaller.new(e.symbolic_call).to_lines.join("\n")}\n```" }
+ RaaP.logger.debug("Exception: #{e.exception.detailed_message}")
RaaP.logger.debug(e.exception.backtrace.join("\n"))
end
end
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts
- puts "success: #{stats.success}, skip: #{stats.skip}, exception: #{stats.exception}"
- puts
+ time_diff = end_time - start_time
+ time = ", time: #{(time_diff * 1000).round}ms"
+ stats_log = "success: #{stats.success}, skip: #{stats.skip}, exception: #{stats.exception}#{time}"
+ RaaP.logger.info(stats_log)
- [status, method_name, method_type]
+ if status == 0 && stats.success.zero? && !stats.break
+ status = 1
+ reason = StringIO.new
+ reason.puts "Never succeeded => #{stats_log}"
+ end
+
+ [status, method_name, method_type, reason]
end
end
end