diff --git a/bin/iruby b/bin/iruby deleted file mode 100755 index 5855534..0000000 --- a/bin/iruby +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift File.expand_path(__dir__ + '/../lib') -require 'iruby/command' -IRuby::Command.new(ARGV).run diff --git a/exe/iruby b/exe/iruby new file mode 100755 index 0000000..a754263 --- /dev/null +++ b/exe/iruby @@ -0,0 +1,7 @@ +#! /usr/bin/env ruby +require "iruby" +require "iruby/application" + +app = IRuby::Application.instance +app.setup +app.run diff --git a/iruby.gemspec b/iruby.gemspec index cb73f28..ae981cb 100644 --- a/iruby.gemspec +++ b/iruby.gemspec @@ -11,7 +11,8 @@ Gem::Specification.new do |s| s.license = 'MIT' s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) - s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } + s.bindir = "exe" + s.executables = %w[iruby] s.test_files = s.files.grep(%r{^test/}) s.require_paths = %w[lib] s.extensions = %w[ext/Rakefile] diff --git a/lib/iruby/application.rb b/lib/iruby/application.rb new file mode 100644 index 0000000..ba71fd8 --- /dev/null +++ b/lib/iruby/application.rb @@ -0,0 +1,380 @@ +require "fileutils" +require "json" +require "optparse" +require "rbconfig" +require "singleton" + +require_relative "error" +require_relative "kernel_app" + +module IRuby + class Application + include Singleton + + # Set the application instance up. + def setup(argv=nil) + @iruby_executable = File.expand_path($PROGRAM_NAME) + parse_command_line(argv) + end + + # Parse the command line arguments + # + # @param argv [Array, nil] The array of arguments. + private def parse_command_line(argv) + argv = ARGV.dup if argv.nil? + @argv = argv # save the original + + case argv[0] + when "help" + # turn `iruby help notebook` into `iruby notebook -h` + argv = [*argv[1..-1], "-h"] + when "version" + # turn `iruby version` into `iruby -v` + argv = ["-v", *argv[1..-1]] + else + argv = argv.dup # prevent to break @argv + end + + opts = OptionParser.new + opts.program_name = "IRuby" + opts.version = ::IRuby::VERSION + opts.banner = "Usage: #{$PROGRAM_NAME} [options] [subcommand] [options]" + + opts.on_tail("-h", "--help") do + print_help(opts) + exit + end + + opts.on_tail("-v", "--version") do + puts opts.ver + exit + end + + opts.order!(argv) + + if argv.length == 0 || argv[0].start_with?("-") + # If no subcommand is given, we use the console + argv = ["console", *argv] + end + + begin + parse_sub_command(argv) if argv.length > 0 + rescue InvalidSubcommandError => err + $stderr.puts err.message + print_help(opts, $stderr) + abort + end + end + + SUB_COMMANDS = { + "register" => "Register IRuby kernel.", + "unregister" => "Unregister the existing IRuby kernel.", + "kernel" => "Launch IRuby kernel", + "console" => "Launch jupyter console with IRuby kernel" + }.freeze.each_value(&:freeze) + + private_constant :SUB_COMMANDS + + private def parse_sub_command(argv) + sub_cmd, *sub_argv = argv + case sub_cmd + when *SUB_COMMANDS.keys + @sub_cmd = sub_cmd.to_sym + @sub_argv = sub_argv + else + raise InvalidSubcommandError.new(sub_cmd, sub_argv) + end + end + + private def print_help(opts, out=$stdout) + out.puts opts.help + out.puts + out.puts "Subcommands" + out.puts "===========" + SUB_COMMANDS.each do |name, description| + out.puts "#{name}" + out.puts " #{description}" + end + end + + def run + case @sub_cmd + when :register + register_kernel(@sub_argv) + when :unregister + unregister_kernel(@sub_argv) + when :console + exec_jupyter(@sub_cmd.to_s, @sub_argv) + when :kernel + @sub_app = KernelApplication.new(@sub_argv) + @sub_app.run + else + raise "[IRuby][BUG] Unknown subcommand: #{@sub_cmd}; this must be treated in parse_command_line." + end + end + + ruby_version_info = RUBY_VERSION.split('.') + DEFAULT_KERNEL_NAME = "ruby#{ruby_version_info[0]}".freeze + DEFAULT_DISPLAY_NAME = "Ruby #{ruby_version_info[0]} (iruby kernel)" + + RegisterParams = Struct.new( + :name, + :display_name, + :profile, + :env, + :user, + :prefix, + :sys_prefix, + :force, + :ipython_dir + ) do + def initialize(*args) + super + self.name ||= DEFAULT_KERNEL_NAME + self.force = false + self.user = true + end + end + + def register_kernel(argv) + params = parse_register_command_line(argv) + + if params.name != DEFAULT_KERNEL_NAME + # `--name` is specified and `--display-name` is not + # default `params.display_name` to `params.name` + params.display_name ||= params.name + end + + check_and_warn_kernel_in_default_ipython_directory(params) + + if installed_kernel_exist?(params.name, params.ipython_dir) + unless params.force + $stderr.puts "IRuby kernel named `#{params.name}` already exists!" + $stderr.puts "Use --force to force register the new kernel." + exit 1 + end + end + + Dir.mktmpdir("iruby_kernel") do |tmpdir| + path = File.join(tmpdir, DEFAULT_KERNEL_NAME) + FileUtils.mkdir_p(path) + + # Stage assets + assets_dir = File.expand_path("../assets", __FILE__) + FileUtils.cp_r(Dir.glob(File.join(assets_dir, "*")), path) + + kernel_dict = { + "argv" => make_iruby_cmd(), + "display_name" => params.display_name || DEFAULT_DISPLAY_NAME, + "language" => "ruby", + "metadata" => {"debugger": false} + } + + # TODO: Support params.profile + # TODO: Support params.env + + kernel_content = JSON.pretty_generate(kernel_dict) + File.write(File.join(path, "kernel.json"), kernel_content) + + args = ["--name=#{params.name}"] + args << "--user" if params.user + args << path + + # TODO: Support params.prefix + # TODO: Support params.sys_prefix + + system("jupyter", "kernelspec", "install", *args) + end + end + + # Warn the existence of the IRuby kernel in the default IPython's kernels directory + private def check_and_warn_kernel_in_default_ipython_directory(params) + default_ipython_kernels_dir = File.expand_path("~/.ipython/kernels") + [params.name, "ruby"].each do |name| + if File.exist?(File.join(default_ipython_kernels_dir, name, "kernel.json")) + warn "IRuby kernel `#{name}` already exists in the deprecated IPython's data directory." + end + end + end + + alias __system__ system + + private def system(*cmdline, dry_run: false) + $stderr.puts "EXECUTE: #{cmdline.map {|x| x.include?(' ') ? x.inspect : x}.join(' ')}" + __system__(*cmdline) unless dry_run + end + + private def installed_kernel_exist?(name, ipython_dir) + kernels_dir = resolve_kernelspec_dir(ipython_dir) + kernel_dir = File.join(kernels_dir, name) + File.file?(File.join(kernel_dir, "kernel.json")) + end + + private def resolve_kernelspec_dir(ipython_dir) + if ENV.has_key?("JUPYTER_DATA_DIR") + if ENV.has_key?("IPYTHONDIR") + warn "both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored." + end + jupyter_data_dir = ENV["JUPYTER_DATA_DIR"] + return File.join(jupyter_data_dir, "kernels") + end + + if ipython_dir.nil? && ENV.key?("IPYTHONDIR") + warn 'IPYTHONDIR is deprecated. Use JUPYTER_DATA_DIR instead.' + ipython_dir = ENV["IPYTHONDIR"] + end + + if ipython_dir + File.join(ipython_dir, 'kerenels') + else + Jupyter.kernelspec_dir + end + end + + private def make_iruby_cmd(executable: nil, extra_arguments: nil) + executable ||= default_executable + extra_arguments ||= [] + [*Array(executable), "kernel", "-f", "{connection_file}", *extra_arguments] + end + + private def default_executable + [RbConfig.ruby, @iruby_executable] + end + + private def parse_register_command_line(argv) + opts = OptionParser.new + opts.banner = "Usage: #{$PROGRAM_NAME} register [options]" + + params = RegisterParams.new + + opts.on( + "--force", + "Force register a new kernel spec. The existing kernel spec will be removed." + ) { params.force = true } + + opts.on( + "--user", + "Register for the current user instead of system-wide." + ) { params.user = true } + + opts.on( + "--name=VALUE", String, + "Specify a name for the kernelspec. This is needed to have multiple IRuby kernels at the same time." + ) {|v| params.name = v } + + opts.on( + "--display-name=VALUE", String, + "Specify the display name for the kernelspec. This is helpful when you have multiple IRuby kernels." + ) {|v| kernel_display_name = v } + + # TODO: --profile + # TODO: --prefix + # TODO: --sys-prefix + # TODO: --env + + define_ipython_dir_option(opts, params) + + opts.order!(argv) + + params + end + + UnregisterParams = Struct.new( + :names, + #:profile, + #:user, + #:prefix, + #:sys_prefix, + :ipython_dir, + :force, + :yes + ) do + def initialize(*args, **kw) + super + self.names = [] + # self.user = true + self.force = false + self.yes = false + end + end + + def unregister_kernel(argv) + params = parse_unregister_command_line(argv) + opts = [] + opts << "-y" if params.yes + opts << "-f" if params.force + system("jupyter", "kernelspec", "uninstall", *opts, *params.names) + end + + private def parse_unregister_command_line(argv) + opts = OptionParser.new + opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options] NAME [NAME ...]" + + params = UnregisterParams.new + + opts.on( + "-f", "--force", + "Force removal, don't prompt for confirmation." + ) { params.force = true} + + opts.on( + "-y", "--yes", + "Answer yes to any prompts." + ) { params.yes = true } + + # TODO: --user + # TODO: --profile + # TODO: --prefix + # TODO: --sys-prefix + + define_ipython_dir_option(opts, params) + + opts.order!(argv) + + params.names = argv.dup + + params + end + + def exec_jupyter(sub_cmd, argv) + opts = OptionParser.new + opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options]" + + kernel_name = resolve_installed_kernel_name(DEFAULT_KERNEL_NAME) + opts.on( + "--kernel=NAME", String, + "The name of the default kernel to start." + ) {|v| kernel_name = v } + + opts.order!(argv) + + opts = ["--kernel=#{kernel_name}"] + exec("jupyter", "console", *opts) + end + + private def resolve_installed_kernel_name(default_name) + kernels = IO.popen(["jupyter", "kernelspec", "list", "--json"], "r", err: File::NULL) do |jupyter_out| + JSON.load(jupyter_out.read) + end + unless kernels["kernelspecs"].key?(default_name) + return "ruby" if kernels["kernelspecs"].key?("ruby") + end + default_name + end + + private def define_ipython_dir_option(opts, params) + opts.on( + "--ipython-dir=DIR", String, + "Specify the IPython's data directory (DEPRECATED)." + ) do |v| + if ENV.key?("JUPYTER_DATA_DIR") + warn 'Both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored.' + else + warn '--ipython-dir is deprecated. Use JUPYTER_DATA_DIR environment variable instead.' + end + + params.ipython_dir = v + end + end + end +end diff --git a/lib/iruby/command.rb b/lib/iruby/command.rb deleted file mode 100644 index 5b0e7c6..0000000 --- a/lib/iruby/command.rb +++ /dev/null @@ -1,190 +0,0 @@ -require 'iruby' - -module IRuby - class Command - def initialize(args) - @args = args - - @ipython_dir = File.expand_path("~/.ipython") - @kernel_dir = resolve_kernelspec_dir.freeze - @kernel_file = File.join(@kernel_dir, 'kernel.json').freeze - @iruby_path = File.expand_path $0 - end - - attr_reader :ipython_dir, :kernel_dir, :kernel_file - - def ipython_kernel_dir - File.join(File.expand_path(@ipython_dir), 'kernels', 'ruby') - end - - def run - case @args.first - when 'version', '-v', '--version' - require 'iruby/version' - puts "IRuby #{IRuby::VERSION}, Ruby #{RUBY_VERSION}" - when 'help', '-h', '--help' - print_help - when 'register' - force_p = @args.include?('--force') - if registered_iruby_path && !force_p - STDERR.puts "#{@kernel_file} already exists!\nUse --force to force a register." - exit 1 - end - register_kernel(force_p) - when 'unregister' - unregister_kernel - when 'kernel' - run_kernel - else - run_ipython - end - end - - private - - def resolve_kernelspec_dir - if ENV.has_key?('JUPYTER_DATA_DIR') - if ENV.has_key?('IPYTHONDIR') - warn 'both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored.' - end - if @args.find {|x| /\A--ipython-dir=/ =~ x } - warn 'both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored.' - end - jupyter_data_dir = ENV['JUPYTER_DATA_DIR'] - return File.join(jupyter_data_dir, 'kernels', 'ruby') - end - - if ENV.has_key?('IPYTHONDIR') - warn 'IPYTHONDIR is deprecated. Use JUPYTER_DATA_DIR instead.' - ipython_dir = ENV['IPYTHONDIR'] - end - - @args.each do |arg| - next unless /\A--ipython-dir=(.*)\Z/ =~ arg - ipython_dir = Regexp.last_match[1] - warn '--ipython-dir is deprecated. Use JUPYTER_DATA_DIR environment variable instead.' - break - end - - if ipython_dir - @ipython_dir = ipython_dir - ipython_kernel_dir - else - File.join(Jupyter.kernelspec_dir, 'ruby') - end - end - - def print_help - puts %{ -Usage: - iruby register Register IRuby kernel in #{@kernel_file}. - iruby unregister Unregister IRuby kernel. - iruby console Launch the IRuby terminal-based console. - iruby notebook Launch the IRuby HTML notebook server. - ... Same as IPython. - -Please note that IRuby accepts the same parameters as IPython. -Try `ipython help` for more information. -} - end - - def run_kernel - IRuby.logger = MultiLogger.new(*Logger.new(STDOUT)) - STDOUT.sync = true # FIXME: This can make the integration test. - - @args.reject! {|arg| arg =~ /\A--log=(.*)\Z/ && IRuby.logger.loggers << Logger.new($1) } - IRuby.logger.level = @args.delete('--debug') ? Logger::DEBUG : Logger::INFO - - raise(ArgumentError, 'Not enough arguments to the kernel') if @args.size < 2 || @args.size > 4 - config_file, boot_file, working_dir = @args[1..-1] - Dir.chdir(working_dir) if working_dir - - require boot_file if boot_file - check_bundler {|e| IRuby.logger.warn "Could not load bundler: #{e.message}" } - - require 'iruby' - Kernel.new(config_file).run - rescue Exception => e - IRuby.logger.fatal "Kernel died: #{e.message}\n#{e.backtrace.join("\n")}" - raise - end - - def check_version - required = '3.0.0' - version = `ipython --version`.chomp - if version < required - STDERR.puts "Your IPython version #{version} is too old, at least #{required} is required" - exit 1 - end - end - - def run_ipython - # If no command is given, we use the console to launch the whole 0MQ-client-server stack - @args = %w(console) + @args if @args.first.to_s !~ /\A\w/ - @args += %w(--kernel ruby) if %w(console qtconsole).include? @args.first - - check_version - check_registered_kernel - check_bundler {|e| STDERR.puts "Could not load bundler: #{e.message}" } - - Process.exec('ipython', *@args) - end - - def check_registered_kernel - if (kernel = registered_iruby_path) - STDERR.puts "#{@iruby_path} differs from registered path #{registered_iruby_path}. -This might not work. Run 'iruby register --force' to fix it." if @iruby_path != kernel - else - register_kernel - end - end - - def check_bundler - require 'bundler' - raise %q{iruby is missing from Gemfile. This might not work. -Add `gem 'iruby'` to your Gemfile to fix it.} unless Bundler.definition.specs.any? {|s| s.name == 'iruby' } - Bundler.setup - rescue LoadError - rescue Exception => e - yield(e) - end - - def register_kernel(force_p=false) - if force_p - unregister_kernel_in_ipython_dir - else - return unless check_existing_kernel_in_ipython_dir - end - FileUtils.mkpath(@kernel_dir) - unless RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin/ - File.write(@kernel_file, MultiJson.dump(argv: [ @iruby_path, 'kernel', '{connection_file}' ], - display_name: "Ruby #{RUBY_VERSION}", language: 'ruby')) - else - ruby_path, iruby_path = [RbConfig.ruby, @iruby_path].map{|path| path.gsub('/', '\\\\')} - File.write(@kernel_file, MultiJson.dump(argv: [ ruby_path, iruby_path, 'kernel', '{connection_file}' ], - display_name: "Ruby #{RUBY_VERSION}", language: 'ruby')) - end - - FileUtils.copy(Dir[File.join(__dir__, 'assets', '*')], @kernel_dir) rescue nil - end - - def check_existing_kernel_in_ipython_dir - return true unless File.file?(File.join(ipython_kernel_dir, 'kernel.json')) - warn "IRuby kernel file already exists in the deprecated IPython's data directory." - warn "Using --force, you can replace the old kernel file with the new one in Jupyter's data directory." - false - end - - def registered_iruby_path - File.exist?(@kernel_file) && MultiJson.load(File.read(@kernel_file))['argv'].first - end - - def unregister_kernel - FileUtils.rm_rf(@kernel_dir) - end - - def unregister_kernel_in_ipython_dir - FileUtils.rm_rf(ipython_kernel_dir) - end - end -end diff --git a/lib/iruby/error.rb b/lib/iruby/error.rb new file mode 100644 index 0000000..0e29571 --- /dev/null +++ b/lib/iruby/error.rb @@ -0,0 +1,12 @@ +module IRuby + class Error < StandardError + end + + class InvalidSubcommandError < Error + def initialize(name, argv) + @name = name + @argv = argv + super("Invalid subcommand name: #{@name}") + end + end +end diff --git a/lib/iruby/kernel_app.rb b/lib/iruby/kernel_app.rb new file mode 100644 index 0000000..98b96b0 --- /dev/null +++ b/lib/iruby/kernel_app.rb @@ -0,0 +1,108 @@ +module IRuby + class KernelApplication + def initialize(argv) + parse_command_line(argv) + end + + def run + if @test_mode + dump_connection_file + return + end + + run_kernel + end + + DEFAULT_CONNECTION_FILE = "kernel-#{Process.pid}.json".freeze + + private def parse_command_line(argv) + opts = OptionParser.new + opts.banner = "Usage: #{$PROGRAM_NAME} [options] [subcommand] [options]" + + @connection_file = nil + opts.on( + "-f CONNECTION_FILE", String, + "JSON file in which to store connection info (default: kernel-.json)" + ) {|v| @connection_file = v } + + @test_mode = false + opts.on( + "--test", + "Run as test mode; dump the connection file and exit." + ) { @test_mode = true } + + @log_file = nil + opts.on( + "--log=FILE", String, + "Specify the log file." + ) {|v| @log_file = v } + + @log_level = Logger::INFO + opts.on( + "--debug", + "Set log-level debug" + ) { @log_level = Logger::DEBUG } + + opts.order!(argv) + + if @connection_file.nil? + # Without -f option, the connection file is at the beginning of the rest arguments + if argv.length <= 3 + @connection_file, @boot_file, @work_dir = argv + else + raise ArgumentError, "Too many comandline arguments" + end + else + if argv.length <= 2 + @boot_file, @work_dir = argv + else + raise ArgumentError, "Too many comandline arguments" + end + end + + @connection_file ||= DEFAULT_CONNECTION_FILE + end + + private def dump_connection_file + puts File.read(@connection_file) + end + + private def run_kernel + IRuby.logger = MultiLogger.new(*Logger.new(STDOUT)) + STDOUT.sync = true # FIXME: This can make the integration test. + + IRuby.logger.loggers << Logger.new(@log_file) unless @log_file.nil? + IRuby.logger.level = @log_level + + if @work_dir + IRuby.logger.debug("iruby kernel") { "Change the working directory: #{@work_dir}" } + Dir.chdir(@work_dir) + end + + if @boot_file + IRuby.logger.debug("iruby kernel") { "Load the boot file: #{@boot_file}" } + require @boot_file + end + + check_bundler {|e| IRuby.logger.warn "Could not load bundler: #{e.message}" } + + require "iruby" + Kernel.new(@connection_file).run + rescue Exception => e + IRuby.logger.fatal "Kernel died: #{e.message}\n#{e.backtrace.join("\n")}" + exit 1 + end + + private def check_bundler + require "bundler" + unless Bundler.definition.specs.any? {|s| s.name == "iruby" } + raise %{IRuby is missing from Gemfile. This might not work. Add `gem "iruby"` in your Gemfile to fix it.} + end + Bundler.setup + rescue LoadError + # do nothing + rescue Exception => e + yield e + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 4801efd..c02d368 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -13,11 +13,11 @@ module IRubyTest class TestBase < Test::Unit::TestCase TEST_DIR = File.expand_path("..", __FILE__).freeze - BIN_DIR = File.expand_path("../bin", TEST_DIR).freeze + EXE_DIR = File.expand_path("../exe", TEST_DIR).freeze LIB_DIR = File.expand_path("../lib", TEST_DIR).freeze RUBY = RbConfig.ruby.freeze - IRUBY_PATH = File.join(BIN_DIR, "iruby").freeze + IRUBY_PATH = File.join(EXE_DIR, "iruby").freeze def iruby_command(*args) [RUBY, "-I#{LIB_DIR}", IRUBY_PATH, *args] diff --git a/test/iruby/application/application_test.rb b/test/iruby/application/application_test.rb new file mode 100644 index 0000000..91d83c9 --- /dev/null +++ b/test/iruby/application/application_test.rb @@ -0,0 +1,32 @@ +require_relative "helper" + +module IRubyTest::ApplicationTests + class ApplicationTest < ApplicationTestBase + DEFAULT_KERNEL_NAME = IRuby::Application::DEFAULT_KERNEL_NAME + DEFAULT_DISPLAY_NAME = IRuby::Application::DEFAULT_DISPLAY_NAME + + def test_help + out, status = Open3.capture2e(*iruby_command("--help")) + assert status.success? + assert_match(/--help/, out) + assert_match(/--version/, out) + assert_match(/^register\b/, out) + assert_match(/^unregister\b/, out) + assert_match(/^kernel\b/, out) + assert_match(/^console\b/, out) + end + + def test_version + out, status = Open3.capture2e(*iruby_command("--version")) + assert status.success? + assert_match(/\bIRuby\s+#{Regexp.escape(IRuby::VERSION)}\b/, out) + end + + def test_unknown_subcommand + out, status = Open3.capture2e(*iruby_command("matz")) + refute status.success? + assert_match(/^Invalid subcommand name: matz$/, out) + assert_match(/^Subcommands$/, out) + end + end +end diff --git a/test/iruby/application/console_test.rb b/test/iruby/application/console_test.rb new file mode 100644 index 0000000..194754e --- /dev/null +++ b/test/iruby/application/console_test.rb @@ -0,0 +1,97 @@ +require_relative "helper" + +module IRubyTest::ApplicationTests + class ConsoleTest < ApplicationTestBase + def setup + Dir.mktmpdir do |tmpdir| + @fake_bin_dir = File.join(tmpdir, "bin") + FileUtils.mkdir_p(@fake_bin_dir) + + @fake_data_dir = File.join(tmpdir, "data") + FileUtils.mkdir_p(@fake_data_dir) + + new_path = [@fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR) + with_env("PATH" => new_path, + "JUPYTER_DATA_DIR" => @fake_data_dir) do + yield + end + end + end + + sub_test_case("there is the default IRuby kernel in JUPYTER_DATA_DIR") do + def setup + super do + ensure_iruby_kernel_is_installed + yield + end + end + + test("run `jupyter console` with the default IRuby kernel") do + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_match(/^Jupyter console [\d\.]+$/, out) + assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out) + end + end + + # NOTE: this case checks the priority of the default IRuby kernel when both kernels are available + sub_test_case("there are both the default IRuby kernel and IRuby kernel named `ruby` in JUPYTER_DATA_DIR") do + def setup + super do + ensure_iruby_kernel_is_installed + ensure_iruby_kernel_is_installed("ruby") + yield + end + end + + test("run `jupyter console` with the default IRuby kernel") do + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_match(/^Jupyter console [\d\.]+$/, out) + assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out) + end + end + + # NOTE: this case checks the availability of the old kernel name + sub_test_case("there is the IRuby kernel, which is named `ruby`, in JUPYTER_DATA_DIR") do + def setup + super do + ensure_iruby_kernel_is_installed("ruby") + yield + end + end + + test("run `jupyter console` with the IRuby kernel `ruby`") do + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_match(/^Jupyter console [\d\.]+$/, out) + assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out) + end + end + + sub_test_case("with --kernel option") do + test("run `jupyter console` command with the given kernel name") do + kernel_name = "other-kernel-#{Process.pid}" + out, status = Open3.capture2e(*iruby_command("console", "--kernel=#{kernel_name}")) + refute status.success? + assert_match(/\bNo such kernel named #{Regexp.escape(kernel_name)}\b/, out) + end + end + + sub_test_case("no subcommand") do + def setup + super do + ensure_iruby_kernel_is_installed + yield + end + end + + test("Run jupyter console command with the default IRuby kernel") do + out, status = Open3.capture2e(*iruby_command, in: :close) + assert status.success? + assert_match(/^Jupyter console [\d\.]+$/, out) + assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out) + end + end + end +end diff --git a/test/iruby/application/helper.rb b/test/iruby/application/helper.rb new file mode 100644 index 0000000..ef47146 --- /dev/null +++ b/test/iruby/application/helper.rb @@ -0,0 +1,37 @@ +require "helper" +require "iruby/application" +require "open3" +require "rbconfig" + +module IRubyTest + module ApplicationTests + class ApplicationTestBase < TestBase + DEFAULT_KERNEL_NAME = IRuby::Application::DEFAULT_KERNEL_NAME + DEFAULT_DISPLAY_NAME = IRuby::Application::DEFAULT_DISPLAY_NAME + + def ensure_iruby_kernel_is_installed(kernel_name=nil) + if kernel_name + system(*iruby_command("register", "--name=#{kernel_name}"), out: File::NULL, err: File::NULL) + else + system(*iruby_command("register"), out: File::NULL, err: File::NULL) + kernel_name = DEFAULT_KERNEL_NAME + end + kernel_json = File.join(ENV["JUPYTER_DATA_DIR"], "kernels", kernel_name, "kernel.json") + assert_path_exist kernel_json + + # Insert -I option to add the lib directory in the $LOAD_PATH of the kernel process + modified_content = JSON.load(File.read(kernel_json)) + kernel_index = modified_content["argv"].index("kernel") + modified_content["argv"].insert(kernel_index - 1, "-I#{LIB_DIR}") + File.write(kernel_json, JSON.pretty_generate(modified_content)) + end + + def add_kernel_options(*additional_argv) + kernel_json = File.join(ENV["JUPYTER_DATA_DIR"], "kernels", DEFAULT_KERNEL_NAME, "kernel.json") + modified_content = JSON.load(File.read(kernel_json)) + modified_content["argv"].concat(additional_argv) + File.write(kernel_json, JSON.pretty_generate(modified_content)) + end + end + end +end diff --git a/test/iruby/application/kernel_test.rb b/test/iruby/application/kernel_test.rb new file mode 100644 index 0000000..007af39 --- /dev/null +++ b/test/iruby/application/kernel_test.rb @@ -0,0 +1,93 @@ +require_relative "helper" + +module IRubyTest::ApplicationTests + class KernelTest < ApplicationTestBase + def setup + Dir.mktmpdir do |tmpdir| + @fake_bin_dir = File.join(tmpdir, "bin") + FileUtils.mkdir_p(@fake_bin_dir) + + @fake_data_dir = File.join(tmpdir, "data") + FileUtils.mkdir_p(@fake_data_dir) + + new_path = [@fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR) + with_env("PATH" => new_path, + "JUPYTER_DATA_DIR" => @fake_data_dir) do + ensure_iruby_kernel_is_installed + yield + end + end + end + + test("--test option dumps the given connection file") do + connection_info = { + "control_port" => 123456, + "shell_port" => 123457, + "transport" => "tcp", + "signature_scheme" => "hmac-sha256", + "stdin_port" => 123458, + "hb_port" => 123459, + "ip" => "127.0.0.1", + "iopub_port" => 123460, + "key" => "a0436f6c-1916-498b-8eb9-e81ab9368e84" + } + Dir.mktmpdir do |tmpdir| + connection_file = File.join(tmpdir, "connection.json") + File.write(connection_file, JSON.dump(connection_info)) + out, status = Open3.capture2e(*iruby_command("kernel", "-f", connection_file, "--test")) + assert status.success? + assert_equal connection_info, JSON.load(out) + end + end + + test("the default log level is INFO") do + Dir.mktmpdir do |tmpdir| + boot_file = File.join(tmpdir, "boot.rb") + File.write(boot_file, <<~BOOT_SCRIPT) + puts "!!! INFO: \#{Logger::INFO}" + puts "!!! LOG LEVEL: \#{IRuby.logger.level}" + puts "!!! LOG LEVEL IS INFO: \#{IRuby.logger.level == Logger::INFO}" + BOOT_SCRIPT + + add_kernel_options(boot_file) + + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_match(/^!!! LOG LEVEL IS INFO: true$/, out) + end + end + + test("--debug option makes the log level DEBUG") do + Dir.mktmpdir do |tmpdir| + boot_file = File.join(tmpdir, "boot.rb") + File.write(boot_file, <<~BOOT_SCRIPT) + puts "!!! LOG LEVEL IS DEBUG: \#{IRuby.logger.level == Logger::DEBUG}" + BOOT_SCRIPT + + add_kernel_options("--debug", boot_file) + + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_match(/^!!! LOG LEVEL IS DEBUG: true$/, out) + end + end + + test("--log option adds a log destination file") do + Dir.mktmpdir do |tmpdir| + boot_file = File.join(tmpdir, "boot.rb") + File.write(boot_file, <<~BOOT_SCRIPT) + IRuby.logger.info("bootfile") { "!!! LOG MESSAGE FROM BOOT FILE !!!" } + BOOT_SCRIPT + + log_file = File.join(tmpdir, "log.txt") + + add_kernel_options("--log=#{log_file}", boot_file) + + out, status = Open3.capture2e(*iruby_command("console"), in: :close) + assert status.success? + assert_path_exist log_file + assert_match(/\bINFO -- bootfile: !!! LOG MESSAGE FROM BOOT FILE !!!$/, File.read(log_file)) + end + end + end +end diff --git a/test/iruby/application/register_test.rb b/test/iruby/application/register_test.rb new file mode 100644 index 0000000..f7ad9b4 --- /dev/null +++ b/test/iruby/application/register_test.rb @@ -0,0 +1,139 @@ +require_relative "helper" + +module IRubyTest::ApplicationTests + class RegisterTest < ApplicationTestBase + sub_test_case("when the existing IRuby kernel is in IPython's kernels directory") do + def setup + Dir.mktmpdir do |tmpdir| + ipython_dir = File.join(tmpdir, ".ipython") + # prepare the existing IRuby kernel with the default name + with_env("JUPYTER_DATA_DIR" => ipython_dir) do + ensure_iruby_kernel_is_installed + end + + fake_bin_dir = File.join(tmpdir, "bin") + fake_jupyter = File.join(fake_bin_dir, "jupyter") + FileUtils.mkdir_p(fake_bin_dir) + IO.write(fake_jupyter, <<-FAKE_JUPYTER) +#!/usr/bin/env ruby +puts "Fake Jupyter" + FAKE_JUPYTER + File.chmod(0o755, fake_jupyter) + + new_path = [fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR) + with_env( + "HOME" => tmpdir, + "PATH" => new_path, + "JUPYTER_DATA_DIR" => nil, + "IPYTHONDIR" => nil + ) do + yield + end + end + end + + test("IRuby warns tthe existence of the kernel in IPython's kerenls directory and executes `jupyter kernelspec install` command") do + out, status = Open3.capture2e(*iruby_command("register")) + assert status.success? + assert_match(/^Fake Jupyter$/, out) + assert_match(/^#{Regexp.escape("IRuby kernel `#{DEFAULT_KERNEL_NAME}` already exists in the deprecated IPython's data directory.")}$/, + out) + end + end + + sub_test_case("when the existing IRuby kernel is in Jupyter's default kernels directory") do + # TODO + end + + sub_test_case("JUPYTER_DATA_DIR is supplied") do + def setup + Dir.mktmpdir do |tmpdir| + @kernel_json = File.join(tmpdir, "kernels", DEFAULT_KERNEL_NAME, "kernel.json") + with_env( + "JUPYTER_DATA_DIR" => tmpdir, + "IPYTHONDIR" => nil + ) do + yield + end + end + end + + test("a new IRuby kernel `#{DEFAULT_KERNEL_NAME}` will be installed in JUPYTER_DATA_DIR") do + assert_path_not_exist @kernel_json + + out, status = Open3.capture2e(*iruby_command("register")) + assert status.success? + assert_path_exist @kernel_json + + kernel = JSON.load(File.read(@kernel_json)) + assert_equal DEFAULT_DISPLAY_NAME, kernel["display_name"] + end + + sub_test_case("there is a IRuby kernel in JUPYTER_DATA_DIR") do + def setup + super do + FileUtils.mkdir_p(File.dirname(@kernel_json)) + File.write(@kernel_json, '"dummy kernel"') + assert_equal '"dummy kernel"', File.read(@kernel_json) + yield + end + end + + test("warn the existence of the kernel") do + out, status = Open3.capture2e(*iruby_command("register")) + refute status.success? + assert_match(/^#{Regexp.escape("IRuby kernel named `#{DEFAULT_KERNEL_NAME}` already exists!")}$/, + out) + assert_match(/^#{Regexp.escape("Use --force to force register the new kernel.")}$/, + out) + end + + test("the existing kernel is not overwritten") do + _out, status = Open3.capture2e(*iruby_command("register")) + refute status.success? + assert_equal '"dummy kernel"', File.read(@kernel_json) + end + + sub_test_case("`--force` option is specified") do + test("the existing kernel is overwritten by the new kernel") do + out, status = Open3.capture2e(*iruby_command("register", "--force")) + assert status.success? + assert_not_match(/^#{Regexp.escape("IRuby kernel named `#{DEFAULT_KERNEL_NAME}` already exists!")}$/, + out) + assert_not_match(/^#{Regexp.escape("Use --force to force register the new kernel.")}$/, + out) + assert_not_equal '"dummy kernel"', File.read(@kernel_json) + end + end + end + end + + sub_test_case("both JUPYTER_DATA_DIR and IPYTHONDIR are supplied") do + def setup + Dir.mktmpdir do |tmpdir| + Dir.mktmpdir do |tmpdir2| + with_env( + "JUPYTER_DATA_DIR" => tmpdir, + "IPYTHONDIR" => tmpdir2 + ) do + yield + end + end + end + end + + test("warn for IPYTHONDIR") do + out, status = Open3.capture2e(*iruby_command("register")) + assert status.success? + assert_match(/^#{Regexp.escape("both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored.")}$/, + out) + end + + test("a new kernel is installed in JUPYTER_DATA_DIR") do + _out, status = Open3.capture2e(*iruby_command("register")) + assert status.success? + assert_path_exist File.join(ENV["JUPYTER_DATA_DIR"], "kernels", DEFAULT_KERNEL_NAME, "kernel.json") + end + end + end +end diff --git a/test/iruby/application/unregister_test.rb b/test/iruby/application/unregister_test.rb new file mode 100644 index 0000000..a04a5a5 --- /dev/null +++ b/test/iruby/application/unregister_test.rb @@ -0,0 +1,77 @@ +require_relative "helper" + +module IRubyTest::ApplicationTests + class UnregisterTest < ApplicationTestBase + def setup + Dir.mktmpdir do |tmpdir| + @kernel_json = File.join(tmpdir, "kernels", DEFAULT_KERNEL_NAME, "kernel.json") + with_env( + "JUPYTER_DATA_DIR" => tmpdir, + "IPYTHONDIR" => nil + ) do + yield + end + end + end + + sub_test_case("when there is no IRuby kernel in JUPYTER_DATA_DIR") do + test("the command succeeds") do + assert system(*iruby_command("unregister", "-f", DEFAULT_KERNEL_NAME), + out: File::NULL, err: File::NULL) + end + end + + sub_test_case("when the existing IRuby kernel in JUPYTER_DATA_DIR") do + def setup + super do + ensure_iruby_kernel_is_installed + yield + end + end + + test("uninstall the existing kernel") do + assert system(*iruby_command("unregister", "-f", DEFAULT_KERNEL_NAME), + out: File::NULL, err: File::NULL) + assert_path_not_exist @kernel_json + end + end + + sub_test_case("when the existing IRuby kernel in IPython's kernels directory") do + def setup + super do + Dir.mktmpdir do |tmpdir| + ipython_dir = File.join(tmpdir, ".ipython") + + # prepare the existing IRuby kernel with the default name + with_env("JUPYTER_DATA_DIR" => ipython_dir) do + ensure_iruby_kernel_is_installed + end + + fake_bin_dir = File.join(tmpdir, "bin") + fake_jupyter = File.join(fake_bin_dir, "jupyter") + FileUtils.mkdir_p(fake_bin_dir) + IO.write(fake_jupyter, <<-FAKE_JUPYTER) + #!/usr/bin/env ruby + puts "Fake Jupyter" + FAKE_JUPYTER + File.chmod(0o755, fake_jupyter) + + new_path = [fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR) + with_env( + "HOME" => tmpdir, + "PATH" => new_path, + "IPYTHONDIR" => nil + ) do + yield + end + end + end + end + + test("the kernel in IPython's kernels directory is not removed") do + assert system(*iruby_command("unregister", "-f"), out: File::NULL, err: File::NULL) + assert_path_exist File.join(File.expand_path("~/.ipython"), "kernels", DEFAULT_KERNEL_NAME, "kernel.json") + end + end + end +end diff --git a/test/iruby/command_test.rb b/test/iruby/command_test.rb deleted file mode 100644 index ded6922..0000000 --- a/test/iruby/command_test.rb +++ /dev/null @@ -1,209 +0,0 @@ -require 'iruby/command' - -module IRubyTest - class CommandTest < TestBase - def test_with_empty_argv - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => nil) do - assert_output(nil, /\A\z/) do - @command = IRuby::Command.new([]) - kernel_dir = File.join(IRuby::Jupyter.kernelspec_dir, 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - - def test_with_JUPYTER_DATA_DIR - Dir.mktmpdir do |tmpdir| - with_env('JUPYTER_DATA_DIR' => tmpdir, - 'IPYTHONDIR' => nil) do - assert_output(nil, /\A\z/) do - @command = IRuby::Command.new([]) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - end - - def test_with_IPYTHONDIR - Dir.mktmpdir do |tmpdir| - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => tmpdir) do - assert_output(nil, /IPYTHONDIR is deprecated\. Use JUPYTER_DATA_DIR instead\./) do - @command = IRuby::Command.new([]) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - end - - def test_with_JUPYTER_DATA_DIR_and_IPYTHONDIR - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env('JUPYTER_DATA_DIR' => tmpdir, - 'IPYTHONDIR' => tmpdir2) do - assert_output(nil, /both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored\./) do - @command = IRuby::Command.new([]) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - end - end - - def test_with_ipython_dir_option - Dir.mktmpdir do |tmpdir| - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => nil) do - assert_output(nil, /--ipython-dir is deprecated\. Use JUPYTER_DATA_DIR environment variable instead\./) do - @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir}]) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - end - - def test_with_JUPYTER_DATA_DIR_and_ipython_dir_option - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env('JUPYTER_DATA_DIR' => tmpdir, - 'IPYTHONDIR' => nil) do - assert_output(nil, /both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored\./) do - @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir2}]) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - assert_equal(kernel_dir, @command.kernel_dir) - assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file) - end - end - end - end - end - - def test_register_when_there_is_kernel_in_ipython_dir - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => nil, - 'HOME' => tmpdir2) do - ignore_warning do - @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"]) - assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file) - @command.run - assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json")) - end - end - - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => nil, - 'HOME' => tmpdir2) do - @command = IRuby::Command.new(["register"]) - assert_output(nil, /IRuby kernel file already exists in the deprecated IPython's data directory\.\nUsing --force, you can replace the old kernel file with the new one in Jupyter's data directory\./) do - @command.run - end - assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json'))) - refute(File.file?(@command.kernel_file)) - - @command = IRuby::Command.new(["register", "--force"]) - assert_output(nil, nil) do - @command.run - end - refute(File.exist?(@command.ipython_kernel_dir)) - assert(File.file?(@command.kernel_file)) - end - end - end - end - - def test_register_and_unregister_with_JUPYTER_DATA_DIR - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env('JUPYTER_DATA_DIR' => tmpdir, - 'IPYTHONDIR' => nil, - 'HOME' => tmpdir2) do - assert_output(nil, nil) do - @command = IRuby::Command.new(['register']) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - kernel_file = File.join(kernel_dir, 'kernel.json') - assert(!File.file?(kernel_file)) - - @command.run - assert(File.file?(kernel_file)) - - @command = IRuby::Command.new(['unregister']) - @command.run - assert(!File.file?(kernel_file)) - end - end - end - end - end - - def test_register_and_unregister_with_JUPYTER_DATA_DIR_when_there_is_kernel_in_ipython_dir - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env("JUPYTER_DATA_DIR" => nil, - "IPYTHONDIR" => nil, - "HOME" => tmpdir2) do - ignore_warning do - @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"]) - assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file) - @command.run - assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json")) - end - end - - with_env('JUPYTER_DATA_DIR' => tmpdir, - 'IPYTHONDIR' => nil, - 'HOME' => tmpdir2) do - @command = IRuby::Command.new(["register"]) - assert_output(nil, /IRuby kernel file already exists in the deprecated IPython's data directory\.\nUsing --force, you can replace the old kernel file with the new one in Jupyter's data directory\./) do - @command.run - end - assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json'))) - refute(File.file?(@command.kernel_file)) - - @command = IRuby::Command.new(["register", "--force"]) - assert_output(nil, nil) do - @command.run - end - refute(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json'))) - assert(File.file?(@command.kernel_file)) - end - end - end - end - - def test_register_and_unregister_with_IPYTHONDIR - Dir.mktmpdir do |tmpdir| - Dir.mktmpdir do |tmpdir2| - with_env('JUPYTER_DATA_DIR' => nil, - 'IPYTHONDIR' => tmpdir, - 'HOME' => tmpdir2) do - ignore_warning do - @command = IRuby::Command.new(['register']) - kernel_dir = File.join(tmpdir, 'kernels', 'ruby') - kernel_file = File.join(kernel_dir, 'kernel.json') - assert(!File.file?(kernel_file)) - - @command.run - assert(File.file?(kernel_file)) - - @command = IRuby::Command.new(['unregister']) - @command.run - assert(!File.file?(kernel_file)) - end - end - end - end - end - end -end