iruby 0.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +35 -10
  3. data/Gemfile +4 -0
  4. data/README.md +115 -84
  5. data/Rakefile +26 -0
  6. data/ci/Dockerfile.base.erb +41 -0
  7. data/ci/Dockerfile.main.erb +9 -0
  8. data/ci/requirements.txt +1 -0
  9. data/docker/setup.sh +15 -0
  10. data/docker/test.sh +7 -0
  11. data/iruby.gemspec +4 -2
  12. data/lib/iruby.rb +13 -7
  13. data/lib/iruby/command.rb +67 -11
  14. data/lib/iruby/input/README.md +299 -0
  15. data/lib/iruby/jupyter.rb +76 -0
  16. data/lib/iruby/kernel.rb +49 -9
  17. data/lib/iruby/ostream.rb +4 -0
  18. data/lib/iruby/session.rb +116 -0
  19. data/lib/iruby/session/cztop.rb +4 -0
  20. data/lib/iruby/session/rbczmq.rb +5 -1
  21. data/lib/iruby/session_adapter.rb +68 -0
  22. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  23. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  24. data/lib/iruby/session_adapter/pyzmq_adapter.rb +76 -0
  25. data/lib/iruby/session_adapter/rbczmq_adapter.rb +33 -0
  26. data/lib/iruby/utils.rb +1 -2
  27. data/lib/iruby/version.rb +1 -1
  28. data/run-test.sh +12 -0
  29. data/tasks/ci.rake +65 -0
  30. data/test/integration_test.rb +22 -10
  31. data/test/iruby/command_test.rb +208 -0
  32. data/test/iruby/jupyter_test.rb +28 -0
  33. data/test/iruby/multi_logger_test.rb +1 -1
  34. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  35. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  36. data/test/iruby/session_adapter/rbczmq_adapter_test.rb +37 -0
  37. data/test/iruby/session_adapter/session_adapter_test_base.rb +29 -0
  38. data/test/iruby/session_adapter_test.rb +116 -0
  39. data/test/iruby/session_test.rb +53 -0
  40. data/test/test_helper.rb +44 -1
  41. metadata +72 -12
@@ -0,0 +1,55 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class FfirzmqAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'ffi-rzmq'
6
+ end
7
+
8
+ def send(sock, data)
9
+ data.each_with_index do |part, i|
10
+ sock.send_string(part, i == data.size - 1 ? 0 : ZMQ::SNDMORE)
11
+ end
12
+ end
13
+
14
+ def recv(sock)
15
+ msg = []
16
+ while msg.empty? || sock.more_parts?
17
+ begin
18
+ frame = ''
19
+ rc = sock.recv_string(frame)
20
+ ZMQ::Util.error_check('zmq_msg_recv', rc)
21
+ msg << frame
22
+ rescue
23
+ end
24
+ end
25
+ msg
26
+ end
27
+
28
+ def heartbeat_loop(sock)
29
+ @heartbeat_device = ZMQ::Device.new(sock, sock)
30
+ end
31
+
32
+ private
33
+
34
+ def make_socket(type, protocol, host, port)
35
+ case type
36
+ when :ROUTER, :PUB, :REP
37
+ type = ZMQ.const_get(type)
38
+ else
39
+ if ZMQ.const_defined?(type)
40
+ raise ArgumentError, "Unsupported ZMQ socket type: #{type_symbol}"
41
+ else
42
+ raise ArgumentError, "Invalid ZMQ socket type: #{type_symbol}"
43
+ end
44
+ end
45
+ zmq_context.socket(type).tap do |sock|
46
+ sock.bind("#{protocol}://#{host}:#{port}")
47
+ end
48
+ end
49
+
50
+ def zmq_context
51
+ @zmq_context ||= ZMQ::Context.new
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,76 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class PyzmqAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'pycall'
6
+ @zmq = PyCall.import_module('zmq')
7
+ rescue PyCall::PyError => error
8
+ raise LoadError, error.message
9
+ end
10
+
11
+ class << self
12
+ attr_reader :zmq
13
+ end
14
+
15
+ def make_router_socket(protocol, host, port)
16
+ make_socket(:ROUTER, protocol, host, port)
17
+ end
18
+
19
+ def make_pub_socket(protocol, host, port)
20
+ make_socket(:PUB, protocol, host, port)
21
+ end
22
+
23
+ def make_pub_socket(protocol, host, port)
24
+ make_socket(:REP, protocol, host, port)
25
+ end
26
+
27
+ def heartbeat_loop(sock)
28
+ PyCall.sys.path.append(File.expand_path('../pyzmq', __FILE__))
29
+ heartbeat = PyCall.import_module('iruby.heartbeat')
30
+ @heartbeat_thread = heartbeat.Heartbeat.new(sock)
31
+ @heartbeat_thread.start
32
+ end
33
+
34
+ private
35
+
36
+ def socket_type(type_symbol)
37
+ case type_symbol
38
+ when :ROUTER, :PUB, :REP
39
+ zmq[type_symbol]
40
+ else
41
+ raise ArgumentError, "Unknown ZMQ socket type: #{type_symbol}"
42
+ end
43
+ end
44
+
45
+ def make_socket(type_symbol, protocol, host, port)
46
+ type = socket_type(type_symbol)
47
+ sock = zmq_context.socket(type)
48
+ bind_socket(sock, protocol, host, port)
49
+ sock
50
+ end
51
+
52
+ def bind_socket(sock, protocol, host, port)
53
+ iface = "#{protocol}://#{host}"
54
+ case protocol
55
+ when 'tcp'
56
+ if port <= 0
57
+ port = sock.bind_to_random_port(iface)
58
+ else
59
+ sock.bind("#{iface}:#{port}")
60
+ end
61
+ else
62
+ raise ArgumentError, "Unsupported protocol: #{protocol}"
63
+ end
64
+ [sock, port]
65
+ end
66
+
67
+ def zmq_context
68
+ zmq.Context.instance
69
+ end
70
+
71
+ def zmq
72
+ self.class.zmq
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,33 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class RbczmqAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'rbczmq'
6
+ end
7
+
8
+ def send(sock, data)
9
+ sock.send_message(ZMQ::Message(*data))
10
+ end
11
+
12
+ def recv(sock)
13
+ sock.recv_message
14
+ end
15
+
16
+ def heartbeat_loop(sock)
17
+ ZMQ.proxy(sock, sock)
18
+ end
19
+
20
+ private
21
+
22
+ def make_socket(type, protocol, host, port)
23
+ zmq_context.socket(type).tap do |sock|
24
+ sock.bind("#{protocol}://#{host}:#{port}")
25
+ end
26
+ end
27
+
28
+ def zmq_context
29
+ @zmq_context ||= ZMQ::Context.new
30
+ end
31
+ end
32
+ end
33
+ end
@@ -7,8 +7,7 @@ module IRuby
7
7
  def display(obj, options = {})
8
8
  Kernel.instance.session.send(:publish, :display_data,
9
9
  data: Display.display(obj, options),
10
- metadata: {},
11
- source: 'ruby') unless obj.nil?
10
+ metadata: {}) unless obj.nil?
12
11
  end
13
12
 
14
13
  def table(s, **options)
@@ -1,3 +1,3 @@
1
1
  module IRuby
2
- VERSION = '0.3'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -0,0 +1,12 @@
1
+ #! /bin/bash
2
+
3
+ set -ex
4
+
5
+ export PYTHON=python3
6
+
7
+ ADAPTERS="cztop rbczmq ffi-rzmq pyzmq"
8
+
9
+ for adapter in $ADAPTERS; do
10
+ export IRUBY_TEST_SESSION_ADAPTER_NAME=$adapter
11
+ bundle exec rake test TESTOPTS=-v
12
+ done
@@ -0,0 +1,65 @@
1
+ namespace :ci do
2
+ namespace :docker do
3
+ def ruby_version
4
+ @ruby_version ||= ENV['ruby_version']
5
+ end
6
+
7
+ def ruby_image_name
8
+ @ruby_image_name ||= "rubylang/ruby:#{ruby_version}-bionic"
9
+ end
10
+
11
+ def iruby_test_base_image_name
12
+ @iruby_test_base_image_name ||= "iruby-test-base:ruby-#{ruby_version}"
13
+ end
14
+
15
+ def iruby_test_image_name
16
+ @docker_image_name ||= begin
17
+ "sciruby/iruby-test:ruby-#{ruby_version}"
18
+ end
19
+ end
20
+
21
+ def docker_image_found?(image_name)
22
+ image_id = `docker images -q #{image_name}`.chomp
23
+ image_id.length > 0
24
+ end
25
+
26
+ directory 'tmp'
27
+
28
+ desc "Build iruby-test-base docker image"
29
+ task :build_test_base_image => 'tmp' do
30
+ unless docker_image_found?(iruby_test_base_image_name)
31
+ require 'erb'
32
+ dockerfile_content = ERB.new(File.read('ci/Dockerfile.base.erb')).result(binding)
33
+ File.write('tmp/Dockerfile', dockerfile_content)
34
+ sh 'docker', 'build', '-t', iruby_test_base_image_name, '-f', 'tmp/Dockerfile', '.'
35
+ end
36
+ end
37
+
38
+ desc "Pull docker image of ruby"
39
+ task :pull_ruby_image do
40
+ sh 'docker', 'pull', ruby_image_name
41
+ end
42
+
43
+ desc "Build iruby-test docker image"
44
+ task :build_test_image => 'tmp' do
45
+ require 'erb'
46
+ dockerfile_content = ERB.new(File.read('ci/Dockerfile.main.erb')).result(binding)
47
+ File.write('tmp/Dockerfile', dockerfile_content)
48
+ sh 'docker', 'build', '-t', iruby_test_image_name, '-f', 'tmp/Dockerfile', '.'
49
+ end
50
+
51
+ desc 'before_install script for CI with Docker'
52
+ task :before_install => :pull_ruby_image
53
+ task :before_install => :build_test_base_image
54
+
55
+ desc 'install script for CI with Docker'
56
+ task :install => :build_test_image
57
+
58
+ desc 'main script for CI with Docker'
59
+ task :script do
60
+ volumes = ['-v', "#{Dir.pwd}:/iruby"] if ENV['attach_pwd']
61
+ sh 'docker', 'run', '--rm', '-it', *volumes,
62
+ iruby_test_image_name, 'bash', 'run-test.sh'
63
+ end
64
+ end
65
+ end
@@ -1,40 +1,52 @@
1
1
  require 'test_helper'
2
- require 'open3'
2
+ require 'pty'
3
3
  require 'expect'
4
4
 
5
- class IntegrationTest < IRubyTest
5
+ class IRubyTest::IntegrationTest < IRubyTest::TestBase
6
6
  def setup
7
- @stdin, @stdout, @stderr, @process = Open3.popen3('bin/iruby')
7
+ $expect_verbose = false # make true if you want to dump the output of iruby console
8
+
9
+ @in, @out, pid = PTY.spawn('bin/iruby --simple-prompt')
10
+ @waiter = Thread.start { Process.waitpid(pid) }
8
11
  expect 'In [', 30
9
12
  expect '1'
10
13
  expect ']:'
11
14
  end
12
15
 
13
16
  def teardown
14
- @stdin.close
15
- @stdout.close
16
- @stderr.close
17
- @process.kill
17
+ @in.close
18
+ @out.close
19
+ @waiter.join
18
20
  end
19
21
 
20
22
  def write(input)
21
- @stdin.puts input
23
+ @out.puts input
22
24
  end
23
25
 
24
- def expect(pattern, timeout = 1)
25
- assert @stdout.expect(pattern, timeout), "#{pattern} expected"
26
+ def expect(pattern, timeout = 10)
27
+ assert @in.expect(pattern, timeout), "#{pattern} expected, but timeout"
28
+ end
29
+
30
+ def wait_prompt
31
+ expect 'In ['
32
+ expect ']:'
26
33
  end
27
34
 
28
35
  def test_interaction
36
+ skip "This test too much unstable"
37
+
29
38
  write '"Hello, world!"'
30
39
  expect '"Hello, world!"'
31
40
 
41
+ wait_prompt
32
42
  write 'puts "Hello!"'
33
43
  expect 'Hello!'
34
44
 
45
+ wait_prompt
35
46
  write '12 + 12'
36
47
  expect '24'
37
48
 
49
+ wait_prompt
38
50
  write 'ls'
39
51
  expect 'self.methods'
40
52
  end
@@ -0,0 +1,208 @@
1
+ require 'test_helper'
2
+ require 'iruby/command'
3
+
4
+ module IRubyTest
5
+ class CommandTest < TestBase
6
+ def test_with_empty_argv
7
+ with_env('JUPYTER_DATA_DIR' => nil,
8
+ 'IPYTHONDIR' => nil) do
9
+ assert_output(nil, /\A\z/) do
10
+ @command = IRuby::Command.new([])
11
+ kernel_dir = File.join(IRuby::Jupyter.kernelspec_dir, 'ruby')
12
+ assert_equal(kernel_dir, @command.kernel_dir)
13
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
14
+ end
15
+ end
16
+ end
17
+
18
+ def test_with_JUPYTER_DATA_DIR
19
+ Dir.mktmpdir do |tmpdir|
20
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
21
+ 'IPYTHONDIR' => nil) do
22
+ assert_output(nil, /\A\z/) do
23
+ @command = IRuby::Command.new([])
24
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
25
+ assert_equal(kernel_dir, @command.kernel_dir)
26
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def test_with_IPYTHONDIR
33
+ Dir.mktmpdir do |tmpdir|
34
+ with_env('JUPYTER_DATA_DIR' => nil,
35
+ 'IPYTHONDIR' => tmpdir) do
36
+ assert_output(nil, /IPYTHONDIR is deprecated\. Use JUPYTER_DATA_DIR instead\./) do
37
+ @command = IRuby::Command.new([])
38
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
39
+ assert_equal(kernel_dir, @command.kernel_dir)
40
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_with_JUPYTER_DATA_DIR_and_IPYTHONDIR
47
+ Dir.mktmpdir do |tmpdir|
48
+ Dir.mktmpdir do |tmpdir2|
49
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
50
+ 'IPYTHONDIR' => tmpdir2) do
51
+ assert_output(nil, /both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored\./) do
52
+ @command = IRuby::Command.new([])
53
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
54
+ assert_equal(kernel_dir, @command.kernel_dir)
55
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def test_with_ipython_dir_option
63
+ Dir.mktmpdir do |tmpdir|
64
+ with_env('JUPYTER_DATA_DIR' => nil,
65
+ 'IPYTHONDIR' => nil) do
66
+ assert_output(nil, /--ipython-dir is deprecated\. Use JUPYTER_DATA_DIR environment variable instead\./) do
67
+ @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir}])
68
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
69
+ assert_equal(kernel_dir, @command.kernel_dir)
70
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_with_JUPYTER_DATA_DIR_and_ipython_dir_option
77
+ Dir.mktmpdir do |tmpdir|
78
+ Dir.mktmpdir do |tmpdir2|
79
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
80
+ 'IPYTHONDIR' => nil) do
81
+ assert_output(nil, /both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored\./) do
82
+ @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir2}])
83
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
84
+ assert_equal(kernel_dir, @command.kernel_dir)
85
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def test_register_when_there_is_kernel_in_ipython_dir
93
+ Dir.mktmpdir do |tmpdir|
94
+ Dir.mktmpdir do |tmpdir2|
95
+ with_env('JUPYTER_DATA_DIR' => nil,
96
+ 'IPYTHONDIR' => nil,
97
+ 'HOME' => tmpdir2) do
98
+ ignore_warning do
99
+ @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"])
100
+ assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file)
101
+ @command.run
102
+ assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json"))
103
+ end
104
+ end
105
+
106
+ with_env('JUPYTER_DATA_DIR' => nil,
107
+ 'IPYTHONDIR' => nil,
108
+ 'HOME' => tmpdir2) do
109
+ @command = IRuby::Command.new(["register"])
110
+ 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
111
+ @command.run
112
+ end
113
+ assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
114
+ refute(File.file?(@command.kernel_file))
115
+
116
+ @command = IRuby::Command.new(["register", "--force"])
117
+ assert_output(nil, nil) do
118
+ @command.run
119
+ end
120
+ refute(File.exist?(@command.ipython_kernel_dir))
121
+ assert(File.file?(@command.kernel_file))
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def test_register_and_unregister_with_JUPYTER_DATA_DIR
128
+ Dir.mktmpdir do |tmpdir|
129
+ Dir.mktmpdir do |tmpdir2|
130
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
131
+ 'IPYTHONDIR' => nil,
132
+ 'HOME' => tmpdir2) do
133
+ assert_output(nil, nil) do
134
+ @command = IRuby::Command.new(['register'])
135
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
136
+ kernel_file = File.join(kernel_dir, 'kernel.json')
137
+ assert(!File.file?(kernel_file))
138
+
139
+ @command.run
140
+ assert(File.file?(kernel_file))
141
+
142
+ @command = IRuby::Command.new(['unregister'])
143
+ @command.run
144
+ assert(!File.file?(kernel_file))
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def test_register_and_unregister_with_JUPYTER_DATA_DIR_when_there_is_kernel_in_ipython_dir
152
+ Dir.mktmpdir do |tmpdir|
153
+ Dir.mktmpdir do |tmpdir2|
154
+ with_env('HOME' => tmpdir2) do
155
+ ignore_warning do
156
+ @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"])
157
+ assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file)
158
+ @command.run
159
+ assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json"))
160
+ end
161
+ end
162
+
163
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
164
+ 'IPYTHONDIR' => nil,
165
+ 'HOME' => tmpdir2) do
166
+ @command = IRuby::Command.new(["register"])
167
+ 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
168
+ @command.run
169
+ end
170
+ assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
171
+ refute(File.file?(@command.kernel_file))
172
+
173
+ @command = IRuby::Command.new(["register", "--force"])
174
+ assert_output(nil, nil) do
175
+ @command.run
176
+ end
177
+ refute(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
178
+ assert(File.file?(@command.kernel_file))
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def test_register_and_unregister_with_IPYTHONDIR
185
+ Dir.mktmpdir do |tmpdir|
186
+ Dir.mktmpdir do |tmpdir2|
187
+ with_env('JUPYTER_DATA_DIR' => nil,
188
+ 'IPYTHONDIR' => tmpdir,
189
+ 'HOME' => tmpdir2) do
190
+ ignore_warning do
191
+ @command = IRuby::Command.new(['register'])
192
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
193
+ kernel_file = File.join(kernel_dir, 'kernel.json')
194
+ assert(!File.file?(kernel_file))
195
+
196
+ @command.run
197
+ assert(File.file?(kernel_file))
198
+
199
+ @command = IRuby::Command.new(['unregister'])
200
+ @command.run
201
+ assert(!File.file?(kernel_file))
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end