diff --git a/.travis.yml b/.travis.yml index 7c0a86b..fcf8e69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,4 @@ rvm: before_install: - sudo apt-get update - sudo apt-get install libasound2-dev + - sudo apt-get install timidity diff --git a/Gemfile b/Gemfile index a36febd..cd810f1 100644 --- a/Gemfile +++ b/Gemfile @@ -21,4 +21,5 @@ end group :testing do gem 'rspec' gem 'rake' + gem 'ruby-ogginfo' end diff --git a/Gemfile.lock b/Gemfile.lock index 6fc6805..b4b8fa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,21 +1,14 @@ GEM remote: http://rubygems.org/ specs: - alsa-rawmidi (0.2.14) - ffi (>= 1.0) coderay (0.9.8) colored (1.2) diff-lcs (1.1.3) eventmachine (0.12.10) - ffi (1.0.11) - ffi-coremidi (0.1.7) - ffi (>= 1.0) json (1.6.5) method_source (0.6.7) ruby_parser (>= 2.3.1) - midi-jruby (0.0.12) - midi-winmm (0.1.10) - ffi (>= 1.0) + midiator (0.5.1) pry (0.9.7.4) coderay (~> 0.9.8) method_source (~> 0.6.7) @@ -30,26 +23,23 @@ GEM rspec-expectations (2.8.0) diff-lcs (~> 1.1.2) rspec-mocks (2.8.0) + ruby-ogginfo (0.6.8) ruby_cowsay (0.1.0) ruby_parser (2.3.1) sexp_processor (~> 3.0) sexp_processor (3.0.8) slop (2.1.0) - unimidi (0.3.3) - ffi-coremidi PLATFORMS ruby DEPENDENCIES - alsa-rawmidi colored eventmachine json - midi-jruby - midi-winmm + midiator pry rake rspec + ruby-ogginfo ruby_cowsay - unimidi diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 0000000..7d2af02 --- /dev/null +++ b/spec/README.md @@ -0,0 +1,84 @@ +# Jarvis Testing + +This is where all of the Jarvis tests and testing framework lives. There are a +number of files that will help support writing new tests, including RSpec shared +contexts and rake tasks. + +## Running Tests + +Tests are run using the standard rake command: + + $ rake + +This will fire off an RSpec testing suite with the command line options -cfs +(coloured output and specdoc format). + +## Shared Contexts + +Jarvis has two (technically three but two are usually used in conjunction with +each other) shared contexts that help with testing. + +### "server" and "timidity" + +This context is aimed at making writing tests that communicate with an external +Jarvis process really, really easy. + +If you include these lines (in this order) in your RSpec describe block: + + include_context 'timidity' + include_context 'server' + +You will get given a variety of helper methods and some before and after hooks +will automatically run for you. First, a timidity server will be run in the +background. It won't make any sound, instead it will output everything to an Ogg +Vorbis file. This ogg file can be accessed using the `test_ogg` helper method. + +The `ogginfo` gem is used to extract information about the ogg file. +Documentation for `ogginfo` can be found +[here](https://github.com/moumar/ruby-ogginfo). If `test_ogg` returns nil it +means that either the ogg file does not exist or timidity never wrote to it, +which indicates that communication between Jarvis and timidity is not working. + +When timidity is up and running, a Jarvis process will be spawned. There are +calls to `sleep` involved in booting Jarvis and timidity to give them time to +load up. + +When both of these are running, the content of your test is run. There are two +very useful helper methods that will aid in your test writing: `send_command` +and `last_response`. Both of them are quite self explanatory. `send_command` +will take a string and send it to the server and return the server response. +`last_response` is a way of accessing that response numerous times. + +Here's an example: + +``` ruby + describe "Server" do + it "should respond to start" do + send_command "start" + last_response.should include('successfully') + end + end +``` + +When your test has finished executing the Jarvis and timidity processes will be +killed. + +**Note:** This context is quite noisy. All of the server boot up logs are put +into stdout for debugging purposes (in case something goes wrong, you will want +to look at the server logs). Test output is often very busy with this context. + +### test_server + +The "test_server" context will give you exactly the same `send_command` and +`last_response` helper methods as the "server" context described above, but it +will not spawn external processes. + +This context will monkey patch the MusicServer class so that it does not try and +send any data to clients. Instead, it stores that data and makes it accessible +through the `last_response` helper. + +Tests written with this context will look almost exactly the same as tests +written with the "server" context. The only difference is that there is no +timidity server running and, therefore, no ogg file to test against. This +context is just for doing granular tests against the server object that doesn't +require any interprocess communication. diff --git a/spec/server_spec.rb b/spec/server_spec.rb index b9953f1..b8c94af 100644 --- a/spec/server_spec.rb +++ b/spec/server_spec.rb @@ -2,7 +2,7 @@ # Tell RSpec that we're in the server context. This will give us access to an # instance of MusicServer in the @jarvis instance variable and access to some # specific helper methods. - include_context 'server' + include_context 'test_server' it 'responds to start' do send_command "start" @@ -64,3 +64,38 @@ last_response.should include('ERROR') end end + +# This following testing suite fires up two external processes for each test. +# This is an inherently heavy and time consuming process so there are not many +# specs in it. +# +# The idea here is to test that all of the interprocess communication works. The +# timidity process is told to +describe "External Server Connection" do + include_context 'timidity' + include_context 'server' + + it 'should communicate with timidity' do + send_command 'start' + + stop_jarvis + stop_timidity + + test_ogg.should_not be_nil + test_ogg.length.should be > 0 + end + + Jarvis::Generators::NoteGenerator.generators.each do |generator| + it "#{generator} should communicate with timidity" do + send_command "load #{generator}" + send_command 'start' + send_command 'stop' + + stop_jarvis + stop_timidity + + test_ogg.should_not be_nil + test_ogg.length.should be > 0 + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ecbfc9e..4b8d93a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,16 +6,3 @@ # We don't want to hear what the generators have to say unless we explicitly # state otherwise. Jarvis::Generators::NoteGenerator.stdout = File.open('/dev/null', 'w') - -# Because we aren't running the server inside event machine, we need to override -# the send_data method and keep track of the data that has been sent in the -# current request. -class Jarvis::MusicServer - def sent_data - @sent_data ||= [] - end - - def send_data data - sent_data << data.to_s - end -end diff --git a/spec/support/server.rb b/spec/support/server.rb index 2503875..a09e2e3 100644 --- a/spec/support/server.rb +++ b/spec/support/server.rb @@ -1,32 +1,53 @@ +require 'socket' + shared_context 'server' do - # Helper method to send a command to the Jarvis server. + @@jarvis_host = 'localhost' + @@jarvis_port = 1337 + + # Starts up the external jarvis process and waits for it to boot. Then it + # initiates a connection to the jarvis process under the @@socket variable. + # + # Commands can be sent to this jarvis instance with the send_command method. + def start_jarvis + @@jarvis_pid = fork do + exec "#{Jarvis::ROOTDIR}/bin/jarvis" + end + + sleep(1) + @@socket = TCPSocket.new @@jarvis_host, @@jarvis_port + end + + # Sends a command to the jarvis server that's running and returns the string + # response from the server. def send_command command - @jarvis.receive_data command - @last_response = @jarvis.sent_data.last + @@socket.print command + @@last_response = @@socket.recv 4096 end - # Helper method to check the value of the last command sent. + # Refers to the response from the last command sent to jarvis. def last_response - @last_response + @@last_response end - before :each do - # Set option defaults and override testing to true - Jarvis.options = Jarvis.option_defaults - Jarvis.options[:logfile] = '/dev/null' - Jarvis.options[:testing] = true - Jarvis.log = Jarvis.default_logger + # Stops the external jarvis process and closes the socket connection to it. + def stop_jarvis + if @@jarvis_pid + @@socket.close if @@socket + Process.kill('SIGINT', @@jarvis_pid) + Process.wait(@@jarvis_pid) + @@jarvis_pid = nil + end + end - # Reset the commands array. This stops duplicate command errors. - Jarvis::Command.reset - @jarvis = Jarvis::MusicServer.new nil + # RSpec hook to start jarvis before every test case in this context. + before :each do @last_response = nil + start_jarvis end + # RSpec hook to stop jarvis after every test case in this context. after :each do - @jarvis.unbind - @jarvis = nil + stop_jarvis end - end diff --git a/spec/support/test_server.rb b/spec/support/test_server.rb new file mode 100644 index 0000000..1c3b47a --- /dev/null +++ b/spec/support/test_server.rb @@ -0,0 +1,49 @@ +# This shared context sets up all of the appropriate methods and monkey patches +# that are needed to create and use a MusicServer object without running it +# through EventMachine. + +# Because we aren't running the server inside event machine, we need to override +# the send_data method and keep track of the data that has been sent in the +# current request. +class Jarvis::MusicServer + def sent_data + @sent_data ||= [] + end + + def send_data data + sent_data << data.to_s + end +end + +shared_context 'test_server' do + # Helper method to send a command to the Jarvis server. + def send_command command + @jarvis.receive_data command + @last_response = @jarvis.sent_data.last + end + + # Helper method to check the value of the last command sent. + def last_response + @last_response + end + + before :each do + # Set option defaults and override testing to true + Jarvis.options = Jarvis.option_defaults + Jarvis.options[:logfile] = '/dev/null' + Jarvis.options[:testing] = true + Jarvis.log = Jarvis.default_logger + + # Reset the commands array. This stops duplicate command errors. + Jarvis::Command.reset + + @jarvis = Jarvis::MusicServer.new nil + @last_response = nil + end + + after :each do + @jarvis.unbind + @jarvis = nil + end + +end diff --git a/spec/support/timidity.rb b/spec/support/timidity.rb new file mode 100644 index 0000000..1a2bc01 --- /dev/null +++ b/spec/support/timidity.rb @@ -0,0 +1,53 @@ +require 'ogginfo' +require 'fileutils' + +shared_context "timidity" do + # Starts up timidity in an external process. It is told to output all sounds + # in ogg vorbis format to a test.ogg file defined by the test_ogg_path method + # of this context. + def start_timidity + @@timidity_pid = fork do + exec "timidity -iA -Ov -o #{test_ogg_path}" + end + + sleep(1) + end + + # Stops the external timidity process. + def stop_timidity + if @@timidity_pid + Process.kill('SIGINT', @@timidity_pid) + Process.wait(@@timidity_pid) + @@timidity_pid = nil + end + end + + # The absolute path to the test.ogg file that we are telling timidity to write + # to. + def test_ogg_path + Jarvis::ROOTDIR + '/spec/data/test.ogg' + end + + # Gets the designated test.ogg file from inside spec/data. If the ogg file is + # empty or not there, nil will be returned. This is useful for asserting that + # notes actually made it to the server. + def test_ogg + begin + OggInfo.open(test_ogg_path) + rescue Exception => e + nil + end + end + + # Sets up an rspec hook to start the timidity server before each spec + before :each do + start_timidity + end + + # Sets up an rspec hook to stop the timidity process after each spec + after :each do + stop_timidity + + FileUtils.rm test_ogg_path + end +end