# These are integration tests -- they require a local instance of the Scout server to run. # If you only have the Scout Agent gem, these tests will not run successfully. # # Scout internal note: See documentation in scout_sinatra for running tests. # $VERBOSE=nil $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/../lib' ) $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/..' ) require 'test/unit' require 'lib/scout' require "pty" require "expect" require 'rubygems' require "active_record" require "json" # the data format require "erb" # only for loading rails DB config for now require "logger" require "newrelic_rpm" SCOUT_PATH = '../scout' SINATRA_PATH = '../scout_sinatra' AGENT_DIR = File.expand_path( File.dirname(__FILE__) ) + '/working_dir/' PATH_TO_DATA_FILE = File.join AGENT_DIR, 'history.yml' AGENT_LOG = File.join AGENT_DIR, 'latest_run.log' PLUGINS_PROPERTIES = File.join AGENT_DIR, 'plugins.properties' PATH_TO_TEST_PLUGIN = File.expand_path( File.dirname(__FILE__) ) + '/plugins/temp_plugin.rb' class ScoutTest < Test::Unit::TestCase def setup load_fixtures :clients, :plugins, :accounts, :subscriptions, :plugin_metas clear_tables :plugin_activities, :ar_descriptors, :summaries # delete the existing history file File.unlink(PATH_TO_DATA_FILE) if File.exist?(PATH_TO_DATA_FILE) File.unlink(AGENT_LOG) if File.exist?(AGENT_LOG) File.unlink(PLUGINS_PROPERTIES) if File.exist?(PLUGINS_PROPERTIES) Client.update_all "last_checkin='#{5.days.ago.strftime('%Y-%m-%d %H:%M')}'" # ensures that fields are created # Plugin.update_all "converted_at = '#{5.days.ago.strftime('%Y-%m-%d %H:%M')}'" # clear out RRD files Dir.glob(SCOUT_PATH+'/test/rrdbs/*.rrd').each { |f| File.unlink(f) } @client=Client.find_by_key 'key', :include=>:plugins @plugin=@client.plugins.first # avoid client limit issues assert @client.account.subscription.update_attribute(:clients,100) end def test_should_checkin_during_interactive_install Client.update_all "last_checkin=null" res="" PTY.spawn("bin/scout -s http://localhost:4567 -d #{PATH_TO_DATA_FILE} install ") do | stdin, stdout, pid | begin stdin.expect("Enter the Server Key:", 3) do |response| assert_not_nil response, "Agent didn't print prompt for server key" stdout.puts @client.key # feed the agent the key res=stdin.read.lstrip end rescue Errno::EIO # don't care end end assert res.match(/Attempting to contact the server.+Success!/m), "Output from interactive install session isn't right" assert_in_delta Time.now.utc.to_i, @client.reload.last_ping.to_i, 100 assert_in_delta Time.now.utc.to_i, @client.reload.last_checkin.to_i, 100 end def test_should_run_first_time assert_nil @client.last_ping scout(@client.key) assert_in_delta Time.now.utc.to_i, @client.reload.last_ping.to_i, 100 assert_in_delta Time.now.utc.to_i, @client.reload.last_checkin.to_i, 100 assert_equal 'ping_key', history['directives']['ping_key'] end def test_should_not_run_if_not_time_to_checkin # do an initial checkin...should work test_should_run_first_time prev_checkin = @client.reload.last_checkin sleep 2 scout(@client.key) assert_equal prev_checkin, @client.reload.last_checkin end def test_should_run_when_forced # do an initial checkin...should work test_should_run_first_time prev_checkin = @client.reload.last_checkin sleep 2 scout(@client.key,'-F') assert @client.reload.last_checkin > prev_checkin end # indirect way of assessing reuse: examining log def test_reuse_existing_plan test_should_run_first_time res=scout(@client.key, '-v -ldebug') assert_match "Plan not modified",res end def test_should_write_log_on_checkin assert !File.exist?(AGENT_LOG) test_should_run_first_time assert File.exist?(AGENT_LOG) end def test_should_append_to_log_on_ping test_should_run_first_time assert File.exist?(AGENT_LOG) log_file_size=File.read(AGENT_LOG).size sleep 1 scout(@client.key) assert File.read(AGENT_LOG).size > log_file_size, "log should be longer after ping" end def test_should_use_name_option scout(@client.key,'--name="My New Server"') assert_equal "My New Server", @client.reload.name end def test_should_not_change_name_when_not_provided name=@client.name scout(@client.key) assert_equal name, @client.reload.name end def test_should_retrieve_new_plan end def test_should_checkin_even_if_history_file_not_writeable end def test_should_get_plan_with_blank_history_file # Create a blank history file File.open(PATH_TO_DATA_FILE, 'w+') {|f| f.write('') } scout(@client.key) assert_in_delta Time.now.utc.to_i, @client.reload.last_ping.to_i, 100 assert_in_delta Time.now.utc.to_i, @client.reload.last_checkin.to_i, 100 end def test_should_generate_error_on_plugin_timeout end def test_should_generate_alert prev_alerts = Alert.count load_average = Plugin.find(1) # In the real world, the md5 is taken care of automatically, and private key signing takes place manually. # These extra steps are necessary because we have the Sinatra version of the models, not the Rails version. new_code="class MyPlugin < Scout::Plugin; def build_report; alert('yo'); end; end" load_average.meta.code = new_code load_average.meta.save load_average.signature=<42) end end EOC run_scout_test(code) do |res| assert ":fields=>{:answer=>42}", res end end def test_embedded_options_in_test_mode code=<<-EOC class StarterPlugin < Scout::Plugin OPTIONS=<<-EOS lunch: label: Lunch Time default: 12 EOS def build_report report(:lunch_is_at => option(:lunch)) end end EOC run_scout_test(code) do |res| assert_match ":fields=>{:lunch_is_at=>12}", res end end def test_command_line_options_in_test_mode code=<<-EOC class StarterPlugin < Scout::Plugin OPTIONS=<<-EOS lunch: label: Lunch Time default: 12 EOS def build_report report(:lunch_is_at => option(:lunch)) end end EOC run_scout_test(code, 'lunch=13') do |res| assert_match ':fields=>{:lunch_is_at=>"13"', res end end # Needed to ensure that malformed embedded options don't bork the agent in test mode def test_invalid_embedded_options_in_test_mode code=<<-EOC class StarterPlugin < Scout::Plugin OPTIONS=<<-EOS invalid yaml, oh noes! EOS def build_report report(:answer=>42) end end EOC run_scout_test(code) do |res| assert_match "Problem parsing option definition in the plugin code (ignoring and continuing)", res assert_match ":fields=>{:answer=>42}", res end end def test_plugin_properties code=<<-EOC class LookupTest < Scout::Plugin OPTIONS=<<-EOS foo: default: 0 EOS def build_report report :foo_value=>option(:foo) end end EOC run_scout_test(code, 'foo=13') do |res| assert_match ':fields=>{:foo_value=>"13"', res end properties=<<-EOS # this is a properties file myfoo=99 mybar=100 EOS properties_path=File.join(AGENT_DIR,"plugins.properties") File.open(properties_path,"w") {|f| f.write properties} run_scout_test(code, 'foo=lookup:myfoo') do |res| assert_match ':fields=>{:foo_value=>"99"', res end #cleanup File.unlink(properties_path) end def test_plugin_override override_path=File.join(AGENT_DIR,"#{@plugin.id}.rb") code=<<-EOC class OverrideTest < Scout::Plugin def build_report; report(:foo=>99);end end EOC File.open(override_path,"w"){|f|f.write(code)} scout(@client.key) report=YAML.load(@plugin.reload.last_report_raw) assert report["foo"].is_a?(Array) assert_equal 99, report["foo"].first File.delete(override_path) end #def test_plugin_override_removed # test_plugin_override # # # have to clear the RRD files so it doesn't complain about checking in to quickly # Dir.glob(SCOUT_PATH+'/test/rrdbs/*.rrd').each { |f| File.unlink(f) } # @plugin.rrdb_file.create_database(Time.now, []) # scout(@client.key, "-F") # # report=YAML.load(@plugin.reload.last_report_raw) # assert_nil report["foo"], "report shouldn't contain 'foo' field from the override" # assert report["load"].is_a?(Array) # assert_equal 2, report["load"].first #end def test_local_plugin plugin_count=@client.plugins.count local_path=File.join(AGENT_DIR,"my_local_plugin.rb") code=<<-EOC class LocalPluginTest < Scout::Plugin def build_report; report(:answer=>42);end end EOC File.open(local_path,"w"){|f|f.write(code)} scout(@client.key) assert_equal plugin_count+1, @client.reload.plugins.count, "there should be one additional plugin records -- created from the local plugin" File.delete(local_path) end # Streamer tests # includes two plugins of the same class def test_streamer_plugin_compilation mock_pusher do plugins=[] plugins << create_plugin(@client, "AclPlugin_1", PLUGIN_FIXTURES[:acl][:code], PLUGIN_FIXTURES[:acl][:sig]) plugins << create_plugin(@client, "XYZ Plugin", PLUGIN_FIXTURES[:xyz][:code], PLUGIN_FIXTURES[:xyz][:sig]) plugins << create_plugin(@client, "AclPlugin_2", PLUGIN_FIXTURES[:acl][:code], PLUGIN_FIXTURES[:acl][:sig]) scout(@client.key) # to write the initial history file. Sinatra MUST be running $continue_streaming = true # so the streamer will run once streamer=Scout::Streamer.new("http://none", "bogus_client_key", PATH_TO_DATA_FILE, [@client.plugins.first.id]+plugins.map(&:id), "bogus_streaming_key",nil) # for debugging, make last arg Logger.new(STDOUT) res = Pusher::Channel.streamer_data # via the mock_streamer call assert res.is_a?(Hash) assert res[:plugins].is_a?(Array) assert_equal 4, res[:plugins].size assert_equal 2, res[:plugins][0][:fields][:load] assert_equal 1, res[:plugins][1][:fields][:value] assert_equal 2, res[:plugins][2][:fields][:value] assert_equal 1, res[:plugins][3][:fields][:value] end # end of mock_pusher end # the local plugin shouldn't report def test_streamer_with_local_plugin local_path=File.join(AGENT_DIR,"my_local_plugin.rb") code=<<-EOC class LocalPluginTest < Scout::Plugin def build_report; report(:answer=>42);end end EOC File.open(local_path,"w"){|f|f.write(code)} scout(@client.key) mock_pusher do $continue_streaming = true # so the streamer will run once streamer=Scout::Streamer.new("http://none", "bogus_client_key", PATH_TO_DATA_FILE, [@client.plugins.first.id], "bogus_streaming_key",nil) # for debugging, make last arg Logger.new(STDOUT) res = Pusher::Channel.streamer_data # Pusher::Channel.streamer_data via the mock_streamer call assert res.is_a?(Hash) assert res[:plugins].is_a?(Array) assert_equal 1, res[:plugins].size # this is NOT the local plugin, it's a regular plugin that's already there assert_equal 2, res[:plugins][0][:fields][:load] end # end of mock_pusher end # test streamer starting and stopping def test_streamer_process_management streamer_pid_file = File.join(AGENT_DIR, "scout_streamer.pid") test_should_run_first_time assert !File.exist?(streamer_pid_file) assert @client.update_attribute(:streamer_command, "start,A0000000000123,1,3") scout(@client.key) assert File.exist?(streamer_pid_file) process_id = File.read(streamer_pid_file).to_i assert process_running?(process_id) assert_nil @client.reload.streamer_command sleep 2 assert @client.update_attribute(:streamer_command, "stop") scout(@client.key) assert !File.exist?(streamer_pid_file) sleep 2 # give process time to shut down assert !process_running?(process_id) assert_nil @client.reload.streamer_command end def test_streamer_with_memory mock_pusher(3) do plugin = create_plugin(@client, "memory plugin", PLUGIN_FIXTURES[:memory][:code], PLUGIN_FIXTURES[:memory][:sig]) scout(@client.key) #puts YAML.load(File.read(PATH_TO_DATA_FILE))['memory'].to_yaml $continue_streaming = true # so the streamer will start running # for debugging, make last arg Logger.new(STDOUT) streamer=Scout::Streamer.new("http://none", "bogus_client_key", PATH_TO_DATA_FILE, [plugin.id], "bogus_streaming_key",nil) res = Pusher::Channel.streamer_data # Pusher::Channel.streamer_data via the mock_pusher call assert_equal 3, res[:plugins][0][:fields][:v], "after the two streamer runs, this plugin should report v=3 -- it increments each run" end end ###################### ### Helper Methods ### ###################### # Runs the scout command with the given +key+ and +opts+ string (ex: '-F'). def scout(key, opts = nil, print_output=false) opts = "" unless opts cmd= "bin/scout #{key} -s http://localhost:4567 -d #{PATH_TO_DATA_FILE} #{opts} 2>&1" puts "command: #{cmd}" if print_output output=`#{cmd}` puts output if print_output output end # you can use this, but you have to create the plugin file and clean up afterwards. # Or, you can use the blog version below def scout_test(path_to_test_plugin, opts = String.new) `bin/scout test #{path_to_test_plugin} -d #{PATH_TO_DATA_FILE} #{opts}` end # The preferred way to test the agent in test mode. This creates a plugin file with the code you provide, # runs the agent in test mode, and cleans up the file. def run_scout_test(code,opts=String.new) File.open(PATH_TO_TEST_PLUGIN,"w") do |file| file.write(code) end yield scout_test(PATH_TO_TEST_PLUGIN, opts) ensure File.unlink(PATH_TO_TEST_PLUGIN) end def history YAML.load(File.read(PATH_TO_DATA_FILE)) end def process_running?(pid) begin Process.getpgid( pid ) true rescue Errno::ESRCH false end end # Establishes AR connection def self.connect_ar # ActiveRecord configuration begin $LOAD_PATH << File.join(SCOUT_PATH,'app/models') # get an ActiveRecord connection db_config_path=File.join(SCOUT_PATH,'config/database.yml') db_config=YAML.load(ERB.new(File.read(db_config_path)).result) db_hash=db_config['test'] # Store the full class name (including module namespace) in STI type column # For triggers - before, just the class name and not the module name was stored,resulting in errors in the Rails # app. ActiveRecord::Base.store_full_sti_class = true ActiveRecord::Base.establish_connection(db_hash) # scout models and local models require SINATRA_PATH + '/lib/ar_models.rb' ActiveRecord::Base.default_timezone = :utc puts "Established connection to Scout database :-)" puts " #{Account.count} accounts there (sanity check)" end end # ghetto fixture support def load_fixtures(*table_names) clear_tables(*table_names) table_names.each do |table_name| path = "#{SCOUT_PATH}/test/fixtures/#{table_name}.yml" model_name = ActiveSupport::Inflector.classify table_name model_class = ActiveRecord.const_get(model_name) data = YAML.load_file(path) data.each do |key, value_hash| model_instance = model_class.new model_instance.id = key.hash if !value_hash.has_key?(:id) # accounting for named foreign keys in fixtures, part 1 value_hash.each_pair do |k,v| if model_instance.respond_to?(k+"_id") # accounting for named foreign keys in fixtures, part 2 model_instance.send "#{k}_id=",v.hash else model_instance.send "#{k}=",v end end model_instance.save end end end def clear_tables(*tables) tables.each do |table| ActiveRecord::Base.connection.execute("truncate table #{table}") end end # see scout's rake plugin:sign task to create the signature def create_plugin(client,name, code, signature) p=client.plugins.create(:name=>name) PluginMeta.create(:plugin=>p) p.meta.code=code p.code_md5_signature=Digest::MD5.hexdigest(code) p.signature=signature p.save p.meta.save puts "There was a problem creating '#{name}' plugin: #{p.errors.inspect}" if p.errors.any? p end # this with a block to mock the pusher call. You can access the streamer data through the Pusher::Channel.streamer_data # Must be called with a code block def mock_pusher(num_runs=1) # redefine the trigger! method, so the streamer doesn't loop indefinitely. We can't just mock it, because # we need to set the $continue_streaming=false $num_runs_for_mock_pusher=num_runs Pusher::Channel.module_eval do alias orig_trigger! trigger! def self.streamer_data;@@streamer_data;end # for getting the data back out def trigger!(event_name, data, socket=nil) @num_run_for_tests = @num_run_for_tests ? @num_run_for_tests+1 : 1 # puts "in mock pusher trigger! This is run #{@num_run_for_tests} of #{$num_runs_for_mock_pusher}" @@streamer_data = data if @num_run_for_tests >= $num_runs_for_mock_pusher $continue_streaming=false @num_run_for_tests=nil end end end yield # must be called with a block Pusher::Channel.module_eval do alias trigger! orig_trigger! end end # Use these to create plugins as needed PLUGIN_FIXTURES={ :acl=>{:code=>"class AclPlugin < Scout::Plugin;def build_report; report(:value=>1);end;end", :sig=><{:code=>"class XYZPlugin < Scout::Plugin;def build_report; report(:value=>2);end;end", :sig=><{:code=>"class MemoryPlugin < Scout::Plugin;def build_report; v=memory(:v)||0; report(:v=>v);remember(:v,v+1);end;end", :sig=><