simultaneous 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'simultaneous'
16
+ s.version = '0.2.0'
17
+ s.date = '2011-11-03'
18
+ s.rubyforge_project = 'simultaneous'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "Simultaneous is the background task launcher used by Spontaneous CMS"
23
+ s.description = "Simultaneous is designed for the very specific use case of a small set of users collaborating on editing a single website. Because of that it is optimised for infrequent invocation of very long running publishing tasks and provides an event based messaging system that allows launched tasks to communicate back to the CMS web-server and for that server to then fire off update messages through HTML5 Server-Sent Events."
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Garry Hill"]
29
+ s.email = 'garry@magnetised.info'
30
+ s.homepage = 'http://spontaneouscms.org'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## If your gem includes any executables, list them here.
37
+ s.executables = ["simultaneous-server", "simultaneous-console"]
38
+
39
+ ## Specify any RDoc options here. You'll want to add your README and
40
+ ## LICENSE files to the extra_rdoc_files list.
41
+ s.rdoc_options = ["--charset=UTF-8"]
42
+ s.extra_rdoc_files = %w[README LICENSE]
43
+
44
+ ## List your runtime dependencies here. Runtime dependencies are those
45
+ ## that are needed for an end user to actually USE your code.
46
+ s.add_dependency('eventmachine', [">= 1.0.0.beta.4", "< 2.0"])
47
+ s.add_dependency('rack', [">= 1.0", "< 2.0"])
48
+ s.add_dependency('rack-async', [">= 0.0.1", "< 2.0"])
49
+
50
+ ## List your development dependencies here. Development dependencies are
51
+ ## those that are only needed during development
52
+ s.add_development_dependency('rr', ["~> 1.0.4"])
53
+
54
+ ## Leave this section as-is. It will be automatically generated from the
55
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
56
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
57
+ # = MANIFEST =
58
+ s.files = %w[
59
+ Gemfile
60
+ LICENSE
61
+ README
62
+ Rakefile
63
+ bin/simultaneous-console
64
+ bin/simultaneous-server
65
+ lib/simultaneous.rb
66
+ lib/simultaneous/broadcast_message.rb
67
+ lib/simultaneous/client.rb
68
+ lib/simultaneous/command.rb
69
+ lib/simultaneous/command/client_event.rb
70
+ lib/simultaneous/command/fire.rb
71
+ lib/simultaneous/command/kill.rb
72
+ lib/simultaneous/command/set_pid.rb
73
+ lib/simultaneous/command/task_complete.rb
74
+ lib/simultaneous/connection.rb
75
+ lib/simultaneous/rack.rb
76
+ lib/simultaneous/server.rb
77
+ lib/simultaneous/task.rb
78
+ lib/simultaneous/task_client.rb
79
+ lib/simultaneous/task_description.rb
80
+ simultaneous.gemspec
81
+ test/helper.rb
82
+ test/tasks/example.rb
83
+ test/test_client.rb
84
+ test/test_command.rb
85
+ test/test_connection.rb
86
+ test/test_faf.rb
87
+ test/test_message.rb
88
+ test/test_server.rb
89
+ test/test_task.rb
90
+ ]
91
+ # = MANIFEST =
92
+
93
+ ## Test files will be grabbed from the file list. Make sure the path glob
94
+ ## matches what you actually use.
95
+ s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
96
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
3
+ require 'rr'
4
+ require 'simultaneous'
5
+ require 'fileutils'
6
+
7
+
8
+ $debug = false
9
+
10
+ SOCKET = "/tmp/#{$$}-faf.socket"
11
+
12
+ class MiniTest::Spec
13
+ include RR::Adapters::MiniTest
14
+ end
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
6
+
7
+ require 'simultaneous'
8
+
9
+ class MyTask
10
+ include Simultaneous::Task
11
+
12
+ def run
13
+ puts ARGV[0]
14
+ # 10.times do |i|
15
+ # puts i
16
+ # sleep(1)
17
+ # end
18
+ simultaneous_event("example", "done")
19
+ end
20
+ end
21
+
22
+ MyTask.new.run
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ describe Simultaneous::Client do
4
+ it "should send right command to server" do
5
+ EM.run {
6
+ task = Simultaneous.add_task(:publish, "/publish", {:param1 => "value1", :param2 => "value2"}, 12)
7
+ command = Object.new
8
+ mock(command).domain=("faf.org")
9
+ mock(command).dump { "dumpedcommand" }
10
+ mock(Simultaneous::Command::Fire).new(task, {:param2 => "value3"}) { command }
11
+
12
+ Simultaneous.domain = "faf.org"
13
+ Simultaneous.connection = SOCKET
14
+ Simultaneous::Server.start
15
+
16
+ mock(Simultaneous::Server).receive_data("dumpedcommand") { EM.stop }
17
+
18
+ Thread.new {
19
+ pid = Simultaneous.publish({:param2 => "value3"})
20
+ }.join
21
+ }
22
+ end
23
+
24
+ end
@@ -0,0 +1,72 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ describe Simultaneous::Command do
4
+ before do
5
+ Simultaneous.domain = "example.net"
6
+ @task = Simultaneous::TaskDescription.new(:publish, "/publish", 9, {:param1 => "value1", :param2 => "value2"})
7
+ end
8
+ it "should serialize and deserialize correctly" do
9
+ task = Simultaneous::TaskDescription.new(:publish, "/publish", 9, {:param1 => "value1", :param2 => "value2"})
10
+ cmd = Simultaneous::Command::CommandBase.new(task, {"param2" => "newvalue2", :param3 => "value3"})
11
+ cmd2 = Simultaneous::Command.load(cmd.dump)
12
+ task2 = cmd2.task
13
+ task2.binary.must_equal task.binary
14
+ task2.params.must_equal task.params
15
+ task2.name.must_equal task.name
16
+ cmd.params.must_equal({"param1" => "value1", "param2" => "newvalue2", "param3" => "value3"})
17
+ end
18
+
19
+ it "should only run scripts belonging to the same user as the ruby process" do
20
+ stub(File).exist?("/publish") { true }
21
+ stub(File).exists?("/publish") { true }
22
+ stat = Object.new
23
+ stub(stat).uid { Process.uid + 1 }
24
+ stub(File).stat("/publish") { stat }
25
+ cmd = Simultaneous::Command::Fire.new(@task)
26
+ lambda { cmd.run }.must_raise(Simultaneous::PermissionsError)
27
+ end
28
+
29
+ it "should not raise an error if the binary belongs to this process" do
30
+ stat = Object.new
31
+ stub(stat).uid { Process.uid }
32
+ stub(File).stat("/publish") { stat }
33
+ cmd = Simultaneous::Command::Fire.new(@task)
34
+ cmd.permitted?
35
+ end
36
+
37
+ it "should raise an error if the binary belongs to this process" do
38
+ stat = Object.new
39
+ uid = Process.uid
40
+ stub(stat).uid { uid }
41
+ stub(File).stat("/publish") { stat }
42
+ stub(Process).euid { uid + 1 }
43
+ cmd = Simultaneous::Command::Fire.new(@task)
44
+ lambda { cmd.permitted? }.must_raise(Simultaneous::PermissionsError)
45
+ end
46
+
47
+ it "should give error if binary doesn't exist" do
48
+ stub(File).exist?("/publish") { false }
49
+ stub(File).exists?("/publish") { false }
50
+ stub(File).owned?("/publish") { true }
51
+ cmd = Simultaneous::Command::Fire.new(@task)
52
+ lambda { cmd.run }.must_raise(Simultaneous::FileNotFoundError)
53
+ end
54
+
55
+ it "should raise error if command isn't one of the approved list" do
56
+ cmd = Object.new
57
+ mock(cmd).run.times(0)
58
+ lambda { Simultaneous::Server.run(cmd) }.must_raise(Simultaneous::PermissionsError)
59
+ end
60
+
61
+ it "should work with binaries involving a command" do
62
+ task = Simultaneous::TaskDescription.new(:publish, "/publish all")
63
+ stat = Object.new
64
+ stub(stat).uid { Process.uid }
65
+ stub(File).stat("/publish") { stat }
66
+ stub(File).exist?("/publish") { true }
67
+ stub(File).exists?("/publish") { true }
68
+ cmd = Simultaneous::Command::Fire.new(task)
69
+ cmd.valid?
70
+ end
71
+
72
+ end
@@ -0,0 +1,91 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ describe Simultaneous::Connection do
4
+
5
+ after do
6
+ # FileUtils.rm( "/tmp/socket.sock") if File.exist?( "/tmp/socket.sock")
7
+ end
8
+
9
+ it "should allow me to generate a connection string from host & port values" do
10
+ Simultaneous::Connection.tcp("localhost", 999).must_equal "localhost:999"
11
+ end
12
+ it "should be correct recognise TCP connections" do
13
+ %w(127.0.0.1:9999 localhost:1234 123.239.23.1:9999 host.domain.com:9999).each do |c|
14
+ Simultaneous::Connection.new(c).tcp?.must_be :==, true
15
+ Simultaneous::Connection.new(c).unix?.must_be :==, false
16
+ end
17
+ end
18
+
19
+ it "should be correct recognise UNIX connections" do
20
+ %w(/path/to/socket.sock socket file.sock).each do |c|
21
+ Simultaneous::Connection.new(c).tcp?.must_be :==, false
22
+ Simultaneous::Connection.new(c).unix?.must_be :==, true
23
+ end
24
+ end
25
+
26
+ it "should open an EventMachine TCP server connection" do
27
+ conn = Simultaneous::Connection.new("127.0.0.1:9999")
28
+ handler = Object.new
29
+ mock(EventMachine).start_server("127.0.0.1", 9999, handler)
30
+ conn.start_server(handler)
31
+ end
32
+ it "should open an EventMachine TCP client connection" do
33
+ conn = Simultaneous::Connection.new("127.0.0.1:9999")
34
+ handler = Object.new
35
+ mock(EventMachine).connect("127.0.0.1", 9999, handler)
36
+ conn.async_socket(handler)
37
+ end
38
+
39
+ it "should open an synchronous TCP client connection" do
40
+ conn = Simultaneous::Connection.new("127.0.0.1:9999")
41
+ socket = Object.new
42
+ mock(socket).write("string")
43
+ mock(socket).flush
44
+ mock(socket).close_write
45
+ mock(socket).close
46
+ mock(TCPSocket).new("127.0.0.1", 9999) { socket }
47
+ conn.sync_socket do |s|
48
+ s.write("string")
49
+ end
50
+ end
51
+
52
+ it "should open an EventMachine Unix server connection" do
53
+ conn = Simultaneous::Connection.new("/tmp/socket.sock")
54
+ handler = Object.new
55
+ mock(EventMachine).start_server("/tmp/socket.sock", handler)
56
+ conn.start_server(handler)
57
+ end
58
+ it "should open an EventMachine Unix client connection" do
59
+ conn = Simultaneous::Connection.new("/tmp/socket.sock")
60
+ handler = Object.new
61
+ mock(EventMachine).connect("/tmp/socket.sock", handler)
62
+ conn.async_socket(handler)
63
+ end
64
+
65
+ it "should open an synchronous Unix client connection" do
66
+ conn = Simultaneous::Connection.new("/tmp/socket.sock")
67
+ socket = Object.new
68
+ mock(socket).write("string")
69
+ mock(socket).flush
70
+ mock(socket).close_write
71
+ mock(socket).close
72
+ mock(UNIXSocket).new("/tmp/socket.sock") { socket }
73
+ conn.sync_socket do |s|
74
+ s.write("string")
75
+ end
76
+ end
77
+
78
+ it "should set the correct permissions on the EventMachine server socket" do
79
+ socket = "/tmp/socket.sock"
80
+ gid = "789"
81
+ FileUtils.rm(socket) if File.exist?(socket)
82
+ mock(File).chmod(0770, socket)
83
+ mock(File).chown(nil, gid, socket)
84
+ handler = Module.new
85
+ mock(EventMachine).start_server(socket, handler) { FileUtils.touch(socket) }
86
+ conn = Simultaneous::Connection.new(socket, {:gid => gid})
87
+ conn.start_server(handler)
88
+ FileUtils.rm(socket) if File.exist?(socket)
89
+ end
90
+
91
+ end
data/test/test_faf.rb ADDED
@@ -0,0 +1,43 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ describe Simultaneous do
4
+ it "should translate a hash to command line arguments" do
5
+ Simultaneous.to_arguments({
6
+ :param1 => "value1",
7
+ :param2 => "value2",
8
+ :array => [1, 2, "3 of 4"],
9
+ :hash => {:name => "Fred", :age => 23}
10
+ }).must_equal %(--array=1 2 "3 of 4" --hash=name:"Fred" age:23 --param1="value1" --param2="value2")
11
+ end
12
+ it "should enable mapping of task to a binary" do
13
+ Simultaneous.add_task(:publish, "/path/to/binary")
14
+ Simultaneous.binary(:publish).must_equal "/path/to/binary"
15
+ Simultaneous[:publish].binary.must_equal "/path/to/binary"
16
+ end
17
+
18
+ it "should enable setting of a niceness value for the task" do
19
+ Simultaneous.add_task(:publish1, "/path/to/binary", {:niceness => 10})
20
+ Simultaneous[:publish1].niceness.must_equal 10
21
+ Simultaneous.add_task(:publish2, "/path/to/binary", {:nice => 12})
22
+ Simultaneous[:publish2].niceness.must_equal 12
23
+ end
24
+
25
+
26
+ it "should enable launching a task by its name" do
27
+ Simultaneous.add_task(:publish, "/path/to/binary")
28
+ args = {:param1 => "param1", :param2 => "param2"}
29
+ mock(Simultaneous).fire(:publish, args)
30
+ Simultaneous.publish(args)
31
+ end
32
+
33
+ it "should enable setting of path to socket" do
34
+ Simultaneous.connection = "/tmp/something"
35
+ Simultaneous.connection.must_equal "/tmp/something"
36
+ end
37
+
38
+ it "should enable setting of domain" do
39
+ Simultaneous.domain = "domain_name"
40
+ Simultaneous.domain.must_equal "domain_name"
41
+ end
42
+
43
+ end
@@ -0,0 +1,81 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ describe Simultaneous::BroadcastMessage do
4
+
5
+ describe "when parsing input" do
6
+ before do
7
+ @message = Simultaneous::BroadcastMessage.new
8
+ end
9
+ it "should begin as invalid" do
10
+ @message.valid?.wont_be :==, true
11
+ end
12
+
13
+ it "should recognise the domain: header" do
14
+ @message << "domain: name"
15
+ @message.domain.must_equal "name"
16
+ @message.valid?.wont_be :==, true
17
+ end
18
+
19
+ it "should recognise the event: header" do
20
+ @message << "event: name"
21
+ @message.event.must_equal "name"
22
+ @message.valid?.wont_be :==, true
23
+ end
24
+
25
+ it "should recognise the data: header" do
26
+ @message << "data: a"
27
+ @message.data.must_equal "a"
28
+ @message << "data: b"
29
+ @message.data.must_equal "a\nb"
30
+ @message << "data: c"
31
+ @message.data.must_equal "a\nb\nc"
32
+ @message.valid?.wont_be :==, true
33
+ end
34
+
35
+ it "should ignore comments" do
36
+ @message << ": a"
37
+ @message.data.must_equal ""
38
+ end
39
+
40
+ it "should ignore blank lines" do
41
+ @message << ""
42
+ @message.data.must_equal ""
43
+ end
44
+ end
45
+
46
+ describe "when data has been parsed" do
47
+ before do
48
+ @message = Simultaneous::BroadcastMessage.new
49
+ @message.event = "event"
50
+ @message.domain = "domain"
51
+ @message.data = "line 1\nline 2"
52
+ end
53
+
54
+ it "should be valid" do
55
+ @message.valid?.must_equal true
56
+ end
57
+
58
+ it "should serialise to a SSE-friendly format" do
59
+ @message.to_event.must_equal((<<-SRC).gsub(/^ */, ''))
60
+ domain: domain
61
+ event: event
62
+ data: line 1
63
+ data: line 2
64
+
65
+ SRC
66
+ end
67
+ end
68
+
69
+ describe "when initialising" do
70
+ it "should accept values at initialisation" do
71
+ message = Simultaneous::BroadcastMessage.new({
72
+ :domain => "domain",
73
+ :event => "event",
74
+ :data => "line 1\nline 2"
75
+ })
76
+ message.domain.must_equal "domain"
77
+ message.event.must_equal "event"
78
+ message.data.must_equal "line 1\nline 2"
79
+ end
80
+ end
81
+ end