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)