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