diff --git a/lib/iruby.rb b/lib/iruby.rb index 480cf8c..b42bbf2 100644 --- a/lib/iruby.rb +++ b/lib/iruby.rb @@ -7,6 +7,7 @@ require 'iruby/version' require 'iruby/jupyter' +require 'iruby/event_manager' require 'iruby/kernel' require 'iruby/backend' require 'iruby/ostream' diff --git a/lib/iruby/event_manager.rb b/lib/iruby/event_manager.rb new file mode 100644 index 0000000..730740f --- /dev/null +++ b/lib/iruby/event_manager.rb @@ -0,0 +1,40 @@ +module IRuby + class EventManager + def initialize(available_events) + @available_events = available_events.dup.freeze + @callbacks = available_events.map {|n| [n, []] }.to_h + end + + attr_reader :available_events + + def register(event, &block) + check_available_event(event) + @callbacks[event] << block unless block.nil? + block + end + + def unregister(event, callback) + check_available_event(event) + val = @callbacks[event].delete(callback) + unless val + raise ArgumentError, + "Given callable object #{callback} is not registered as a #{event} callback" + end + val + end + + def trigger(event, *args, **kwargs) + check_available_event(event) + @callbacks[event].each do |fn| + fn.call(*args, **kwargs) + end + end + + private + + def check_available_event(event) + return if @callbacks.key?(event) + raise ArgumentError, "Unknown event name: #{event}", caller + end + end +end diff --git a/lib/iruby/kernel.rb b/lib/iruby/kernel.rb index c43cabd..d05ddfe 100644 --- a/lib/iruby/kernel.rb +++ b/lib/iruby/kernel.rb @@ -1,4 +1,6 @@ module IRuby + ExecutionInfo = Struct.new(:raw_cell, :store_history, :silent) + class Kernel RED = "\e[31m" RESET = "\e[0m" @@ -9,22 +11,32 @@ class Kernel attr_reader :session - def initialize(config_file) + EVENTS = [ + :pre_execute, + :pre_run_cell, + :post_run_cell, + :post_execute + ].freeze + + def initialize(config_file, session_adapter_name=nil) @config = MultiJson.load(File.read(config_file)) IRuby.logger.debug("IRuby kernel start with config #{@config}") Kernel.instance = self - @session = Session.new(@config) + @session = Session.new(@config, session_adapter_name) $stdout = OStream.new(@session, :stdout) $stderr = OStream.new(@session, :stderr) init_parent_process_poller + @events = EventManager.new(EVENTS) @execution_count = 0 @backend = create_backend @running = true end + attr_reader :events + def create_backend PryBackend.new rescue Exception => e @@ -83,8 +95,20 @@ def send_status(status) def execute_request(msg) code = msg[:content]['code'] - @execution_count += 1 if msg[:content]['store_history'] - @session.send(:publish, :execute_input, code: code, execution_count: @execution_count) + store_history = msg[:content]['store_history'] + silent = msg[:content]['silent'] + + @execution_count += 1 if store_history + + unless silent + @session.send(:publish, :execute_input, code: code, execution_count: @execution_count) + end + + events.trigger(:pre_execute) + unless silent + exec_info = ExecutionInfo.new(code, store_history, silent) + events.trigger(:pre_run_cell, exec_info) + end content = { status: :ok, @@ -92,9 +116,10 @@ def execute_request(msg) user_expressions: {}, execution_count: @execution_count } + result = nil begin - result = @backend.eval(code, msg[:content]['store_history']) + result = @backend.eval(code, store_history) rescue SystemExit content[:payload] << { source: :ask_exit } rescue Exception => e @@ -103,6 +128,10 @@ def execute_request(msg) content[:status] = :error content[:execution_count] = @execution_count end + + events.trigger(:post_execute) + events.trigger(:post_run_cell, result) unless silent + @session.send(:reply, :execute_reply, content) @session.send(:publish, :execute_result, data: Display.display(result), diff --git a/lib/iruby/session_adapter.rb b/lib/iruby/session_adapter.rb index 02f9448..75ab29c 100644 --- a/lib/iruby/session_adapter.rb +++ b/lib/iruby/session_adapter.rb @@ -10,6 +10,10 @@ def self.available? false end + def self.load_requirements + # Do nothing + end + def initialize(config) @config = config end @@ -37,12 +41,14 @@ def make_rep_socket(protocol, host, port) require_relative 'session_adapter/ffirzmq_adapter' require_relative 'session_adapter/cztop_adapter' require_relative 'session_adapter/pyzmq_adapter' + require_relative 'session_adapter/test_adapter' def self.select_adapter_class(name=nil) classes = { 'ffi-rzmq' => SessionAdapter::FfirzmqAdapter, 'cztop' => SessionAdapter::CztopAdapter, # 'pyzmq' => SessionAdapter::PyzmqAdapter + 'test' => SessionAdapter::TestAdapter, } if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil)) cls = classes[name] diff --git a/lib/iruby/session_adapter/test_adapter.rb b/lib/iruby/session_adapter/test_adapter.rb new file mode 100644 index 0000000..2050a2b --- /dev/null +++ b/lib/iruby/session_adapter/test_adapter.rb @@ -0,0 +1,49 @@ +require 'iruby/session/mixin' + +module IRuby + module SessionAdapter + class TestAdapter < BaseAdapter + include IRuby::SessionSerialize + + DummySocket = Struct.new(:type, :protocol, :host, :port) + + def initialize(config) + super + + unless config['key'].empty? || config['signature_scheme'].empty? + unless config['signature_scheme'] =~ /\Ahmac-/ + raise "Unknown signature_scheme: #{config['signature_scheme']}" + end + digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1] + @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm)) + end + + @send_callback = nil + @recv_callback = nil + end + + attr_accessor :send_callback, :recv_callback + + def send(sock, data) + unless @send_callback.nil? + @send_callback.call(sock, unserialize(data)) + end + end + + def recv(sock) + unless @recv_callback.nil? + serialize(@recv_callback.call(sock)) + end + end + + def heartbeat_loop(sock) + end + + private + + def make_socket(type, protocol, host, port) + DummySocket.new(type, protocol, host, port) + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb index e62b854..b2ae77c 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,10 +1,53 @@ require "iruby" +require "iruby/logger" +require "json" +require 'multi_json' +require "pathname" require "test/unit" require "test/unit/rr" require "tmpdir" + +IRuby.logger = IRuby::MultiLogger.new(*Logger.new(STDERR, level: Logger::Severity::INFO)) + module IRubyTest class TestBase < Test::Unit::TestCase + def self.startup + @__config_dir = Dir.mktmpdir("iruby-test") + @__config_path = Pathname.new(@__config_dir) + "config.json" + File.write(@__config_path, { + control_port: 50160, + shell_port: 57503, + transport: "tcp", + signature_scheme: "hmac-sha256", + stdin_port: 52597, + hb_port: 42540, + ip: "127.0.0.1", + iopub_port: 40885, + key: "a0436f6c-1916-498b-8eb9-e81ab9368e84" + }.to_json) + + @__original_kernel_instance = IRuby::Kernel.instance + end + + def self.shutdown + FileUtils.remove_entry_secure(@__config_dir) + end + + def self.test_config_filename + @__config_path.to_s + end + + def teardown + IRuby::Kernel.instance = @__original_kernel_instance + end + + def with_session_adapter(session_adapter_name) + IRuby::Kernel.new(self.class.test_config_filename, session_adapter_name) + $stdout = STDOUT + $stderr = STDERR + end + def assert_output(stdout=nil, stderr=nil) flunk "assert_output requires a block to capture output." unless block_given? diff --git a/test/iruby/event_manager_test.rb b/test/iruby/event_manager_test.rb new file mode 100644 index 0000000..4ef0857 --- /dev/null +++ b/test/iruby/event_manager_test.rb @@ -0,0 +1,92 @@ +module IRubyTest + class EventManagerTest < TestBase + def setup + @man = IRuby::EventManager.new([:foo, :bar]) + end + + def test_available_events + assert_equal([:foo, :bar], + @man.available_events) + end + + sub_test_case("#register") do + sub_test_case("known event name") do + def test_register + fn = ->() {} + assert_equal(fn, + @man.register(:foo, &fn)) + end + end + + sub_test_case("unknown event name") do + def test_register + assert_raise_message("Unknown event name: baz") do + @man.register(:baz) {} + end + end + end + end + + sub_test_case("#unregister") do + sub_test_case("no event is registered") do + def test_unregister + fn = ->() {} + assert_raise_message("Given callable object #{fn} is not registered as a foo callback") do + @man.unregister(:foo, fn) + end + end + end + + sub_test_case("the registered callable is given") do + def test_unregister + results = { values: [] } + fn = ->(a) { values << a } + + @man.register(:foo, &fn) + + results[:retval] = @man.unregister(:foo, fn) + + @man.trigger(:foo, 42) + + assert_equal({ + values: [], + retval: fn + }, + results) + end + end + end + + sub_test_case("#trigger") do + sub_test_case("no event is registered") do + def test_trigger + assert_nothing_raised do + @man.trigger(:foo) + end + end + end + + sub_test_case("some events are registered") do + def test_trigger + values = [] + @man.register(:foo) {|a| values << a } + @man.register(:foo) {|a| values << 10*a } + @man.register(:foo) {|a| values << 100+a } + + @man.trigger(:foo, 5) + + assert_equal([5, 50, 105], + values) + end + end + + sub_test_case("unknown event name") do + def test_trigger + assert_raise_message("Unknown event name: baz") do + @man.trigger(:baz, 100) + end + end + end + end + end +end diff --git a/test/iruby/kernel_test.rb b/test/iruby/kernel_test.rb new file mode 100644 index 0000000..af7e1f5 --- /dev/null +++ b/test/iruby/kernel_test.rb @@ -0,0 +1,134 @@ +require "base64" + +module IRubyTest + class KernelTest < TestBase + def setup + super + with_session_adapter("test") + @kernel = IRuby::Kernel.instance + end + + def test_execute_request + obj = Object.new + + class << obj + def to_html + "HTML" + end + + def inspect + "!!! inspect !!!" + end + end + + ::IRubyTest.define_singleton_method(:test_object) { obj } + + msg_types = [] + execute_reply = nil + execute_result = nil + @kernel.session.adapter.send_callback = ->(sock, msg) do + header = msg[:header] + content = msg[:content] + msg_types << header["msg_type"] + case header["msg_type"] + when "execute_reply" + execute_reply = content + when "execute_result" + execute_result = content + end + end + + msg = { + content: { + "code" => "IRubyTest.test_object", + "silent" => false, + "store_history" => false, + "user_expressions" => {}, + "allow_stdin" => false, + "stop_on_error" => true, + } + } + @kernel.execute_request(msg) + + assert_equal({ + msg_types: [ "execute_input", "execute_reply", "execute_result" ], + execute_reply: { + status: "ok", + user_expressions: {}, + }, + execute_result: { + data: { + "text/html" => "HTML", + "text/plain" => "!!! inspect !!!" + }, + metadata: {}, + } + }, + { + msg_types: msg_types, + execute_reply: { + status: execute_reply["status"], + user_expressions: execute_reply["user_expressions"] + }, + execute_result: { + data: execute_result["data"], + metadata: execute_result["metadata"] + } + }) + end + + def test_events_around_of_execute_request + event_history = [] + + @kernel.events.register(:pre_execute) do + event_history << :pre_execute + end + + @kernel.events.register(:pre_run_cell) do |exec_info| + event_history << [:pre_run_cell, exec_info] + end + + @kernel.events.register(:post_execute) do + event_history << :post_execute + end + + @kernel.events.register(:post_run_cell) do |result| + event_history << [:post_run_cell, result] + end + + msg = { + content: { + "code" => "true", + "silent" => false, + "store_history" => false, + "user_expressions" => {}, + "allow_stdin" => false, + "stop_on_error" => true, + } + } + @kernel.execute_request(msg) + + msg = { + content: { + "code" => "true", + "silent" => true, + "store_history" => false, + "user_expressions" => {}, + "allow_stdin" => false, + "stop_on_error" => true, + } + } + @kernel.execute_request(msg) + + assert_equal([ + :pre_execute, + [:pre_run_cell, IRuby::ExecutionInfo.new("true", false, false)], + :post_execute, + [:post_run_cell, true], + :pre_execute, + :post_execute + ], + event_history) + end + end +end diff --git a/test/iruby/session_test.rb b/test/iruby/session_test.rb index 3aee3ba..93677a8 100644 --- a/test/iruby/session_test.rb +++ b/test/iruby/session_test.rb @@ -38,6 +38,7 @@ def test_without_any_session_adapter stub(IRuby::SessionAdapter::CztopAdapter).available? { false } stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { false } stub(IRuby::SessionAdapter::PyzmqAdapter).available? { false } + stub(IRuby::SessionAdapter::TestAdapter).available? { false } assert_raises IRuby::SessionAdapterNotFound do IRuby::Session.new(@session_config) end diff --git a/test/run-test.rb b/test/run-test.rb index e602d65..f9a4ac9 100755 --- a/test/run-test.rb +++ b/test/run-test.rb @@ -14,5 +14,6 @@ require_relative "helper" ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "10000" +ENV["IRUBY_TEST_SESSION_ADAPTER_NAME"] ||= "ffi-rzmq" exit Test::Unit::AutoRunner.run(true, test_dir)