rivet 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ module Rivet
2
+ class Bootstrap
3
+ TEMPLATE_SUB_DIR = "bootstrap"
4
+
5
+ attr_reader :gems, :run_list, :template, :environment
6
+ attr_reader :template_path, :chef_command, :chef_organization
7
+
8
+ def initialize(bootstrap_definition = Hash.new)
9
+ ivars = [
10
+ 'gems','run_list','template','environment',
11
+ 'config_dir','chef_organization']
12
+
13
+ ivars.each do |i|
14
+ if bootstrap_definition.has_key?(i)
15
+ instance_variable_set("@#{i}",bootstrap_definition[i])
16
+ end
17
+ end unless bootstrap_definition.nil?
18
+
19
+ @config_dir ||= "."
20
+ @template ||= "default.erb"
21
+
22
+ set_calculated_attrs
23
+ end
24
+
25
+ def user_data
26
+ @user_data ||= generate_user_data
27
+ end
28
+
29
+ protected
30
+
31
+ def set_calculated_attrs
32
+ @template_path = File.join(@config_dir,TEMPLATE_SUB_DIR)
33
+ @chef_command = "/usr/bin/chef-client -j /etc/chef/first-boot.json -L /root/first_run.log -E #{@environment}"
34
+ @secret_file = File.join(@config_dir,"encrypted_data_bag_secret_#{@environment}")
35
+ @validation_key = File.new(File.join(@config_dir,"#{@chef_organization}-validator.pem")).read
36
+ end
37
+
38
+ def generate_user_data
39
+ config_content = "log_level :info\n"
40
+ config_content << "log_location STDOUT\n"
41
+ config_content << "environment #{environment}\n"
42
+ config_content << "chef_server_url 'https://api.opscode.com/organizations/#{chef_organization}'\n"
43
+ config_content << "validation_client_name '#{chef_organization}-validator'\n"
44
+
45
+ install_gems = String.new
46
+
47
+ gems.each do |gem|
48
+ if gem.size > 1
49
+ install_gems << "gem install #{gem[0]} -v #{gem[1]} --no-rdoc --no-ri\n"
50
+ else
51
+ install_gems << "gem install #{gem[0]} --no-rdoc --no-ri\n"
52
+ end
53
+ end unless gems.nil?
54
+
55
+ first_boot = { :run_list => @run_list.join(",") }.to_json unless @run_list.nil?
56
+
57
+ template = ERB.new File.new(File.join(@template_path,@template)).read
58
+ template.result(binding)
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ module Rivet
2
+ class Client
3
+ def initialize
4
+ end
5
+
6
+ def run(options)
7
+ AwsUtils.set_aws_credentials(options[:profile])
8
+ Rivet::Log.level(options[:log_level])
9
+ Rivet::Utils.ensure_minimum_setup
10
+
11
+ group_def = Rivet::Utils.get_definition(options[:group])
12
+
13
+ Rivet::Utils.die "The #{options[:group]} definition doesn't exist" unless group_def
14
+
15
+ Rivet::Log.info("Checking #{options[:group]} autoscaling definition")
16
+ autoscale_def = Rivet::Autoscale.new(options[:group],group_def)
17
+ autoscale_def.show_differences
18
+
19
+ if options[:sync]
20
+ autoscale_def.sync
21
+ else
22
+ Rivet::Log.info("use the -s [--sync] flag to sync changes")
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+
@@ -0,0 +1,27 @@
1
+ class Hash
2
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
3
+ #
4
+ # h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
5
+ # h2 = { x: { y: [7,8,9] }, z: 'xyz' }
6
+ #
7
+ # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
8
+ # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
9
+ # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) }
10
+ # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
11
+ def deep_merge(other_hash, &block)
12
+ dup.deep_merge!(other_hash, &block)
13
+ end
14
+
15
+ # Same as +deep_merge+, but modifies +self+.
16
+ def deep_merge!(other_hash, &block)
17
+ other_hash.each_pair do |k,v|
18
+ tv = self[k]
19
+ if tv.is_a?(Hash) && v.is_a?(Hash)
20
+ self[k] = tv.deep_merge(v, &block)
21
+ else
22
+ self[k] = block && tv ? block.call(k, tv, v) : v
23
+ end
24
+ end
25
+ self
26
+ end
27
+ end
@@ -0,0 +1,90 @@
1
+ module Rivet
2
+ class LaunchConfig
3
+
4
+ LC_ATTRIBUTES = ['key_name','image_id','instance_type','security_groups','bootstrap']
5
+
6
+ LC_ATTRIBUTES.each do |a|
7
+ attr_reader a.to_sym
8
+ end
9
+
10
+ attr_reader :id_prefix
11
+
12
+ def initialize(spec,id_prefix="rivet_")
13
+ @id_prefix = id_prefix
14
+
15
+ LC_ATTRIBUTES.each do |a|
16
+
17
+ if respond_to? "normalize_#{a}".to_sym
18
+ spec[a] = self.send("normalize_#{a.to_sym}",spec[a])
19
+ end
20
+
21
+ instance_variable_set("@#{a}",spec[a])
22
+ end
23
+ end
24
+
25
+ def user_data
26
+ @user_data ||= Bootstrap.new(bootstrap).user_data
27
+ end
28
+
29
+ def identity
30
+ @identity ||= generate_identity
31
+ end
32
+
33
+ def save
34
+ AwsUtils.verify_security_groups(security_groups)
35
+
36
+ lc_collection = AWS::AutoScaling.new().launch_configurations
37
+
38
+ if lc_collection[identity].exists?
39
+ Rivet::Log.info("Launch configuration #{identity} already exists in AWS")
40
+ else
41
+ options = { :key_pair => key_name, :security_groups => security_groups, :user_data => user_data}
42
+ Rivet::Log.info("Saving launch configuration #{identity} to AWS")
43
+ Rivet::Log.debug("Launch Config options:\n #{options.inspect}")
44
+ lc_collection.create(identity,image_id,instance_type, options)
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def build_identity_string
51
+ identity = LC_ATTRIBUTES.inject(String.new) do |accum,attribute|
52
+ if attribute != 'bootstrap'
53
+ attr_value = self.send(attribute.to_sym) ? self.send(attribute.to_sym) : "\0"
54
+ attr_value = attr_value.join("\t") if attr_value.respond_to?(:join)
55
+ accum << attribute.to_s
56
+ accum << Base64.encode64(attr_value)
57
+ else
58
+ accum << attribute.to_s
59
+ accum << Base64.encode64(user_data ? user_data : "\0")
60
+ end
61
+ accum
62
+ end
63
+ identity
64
+ end
65
+
66
+ def generate_identity
67
+ @id_prefix + Digest::SHA1.hexdigest(build_identity_string)
68
+ end
69
+
70
+ def old_generate_identity
71
+ identity = LC_ATTRIBUTES.inject({}) do |ident_hash,attribute|
72
+ if attribute != 'bootstrap'
73
+ Rivet::Log.debug("Adding #{attribute} : #{self.send(attribute.to_sym)} to identity hash for LaunchConfig")
74
+ ident_hash[attribute] = self.send(attribute.to_sym)
75
+ else
76
+ Rivet::Log.debug("Adding user_data to identity hash for LaunchConfig:\n#{user_data} ")
77
+ ident_hash[attribute] = user_data
78
+ end
79
+ ident_hash
80
+ end
81
+ @id_prefix + Digest::SHA1.hexdigest(Marshal::dump(identity))
82
+ end
83
+
84
+ def normalize_security_groups(groups)
85
+ groups.sort
86
+ end
87
+
88
+ end
89
+ end
90
+
@@ -0,0 +1,52 @@
1
+ module Rivet
2
+
3
+ module Log
4
+
5
+ def self.write(level,message)
6
+ @@log ||= SimpleLogger.instance
7
+ @@log.send(level.to_sym) { message }
8
+ end
9
+
10
+ def self.info(message)
11
+ write('info',message)
12
+ end
13
+
14
+ def self.debug(message)
15
+ write('debug',message)
16
+ end
17
+
18
+ def self.fatal(message)
19
+ write('fatal',message)
20
+ end
21
+
22
+ def self.warn(message)
23
+ write('warn',message)
24
+ end
25
+
26
+ def self.level(level)
27
+ @@log ||= SimpleLogger.instance
28
+ @@log.level = level
29
+ end
30
+
31
+ class SimpleLogger< Logger
32
+ include Singleton
33
+
34
+ def initialize
35
+ @dev = Logger::LogDevice.new(STDOUT)
36
+ super @dev
37
+ @progname = "Rivet"
38
+ @formatter = proc do |sev,datetime,name,msg|
39
+ "[#{name}] [#{datetime}] [#{sev}]: #{msg}\n"
40
+ end
41
+ @datetime_format
42
+ end
43
+
44
+ def close
45
+ @dev.close
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+
52
+
@@ -0,0 +1,68 @@
1
+ module Rivet
2
+ module Utils
3
+ AUTOSCALE_DIR = "autoscale"
4
+
5
+ def self.die(level = 'fatal',message)
6
+ Rivet::Log.write(level,message)
7
+ exit
8
+ end
9
+
10
+ def self.ensure_minimum_setup
11
+ if Dir.exists?(AUTOSCALE_DIR)
12
+ true
13
+ else
14
+ Rivet::Log.info("Creating #{AUTOSCALE_DIR}")
15
+ Dir.mkdir(AUTOSCALE_DIR)
16
+ end
17
+ end
18
+
19
+ # This returns the merged definition given a group
20
+
21
+ def self.get_definition(group)
22
+ defaults = consume_defaults
23
+ group_def = load_definition(group)
24
+ if defaults && group_def
25
+ group_def = defaults.deep_merge(group_def)
26
+ end
27
+ group_def ? group_def : false
28
+ end
29
+
30
+ # Gobbles up the defaults file from YML, returns the hash or false if empty
31
+
32
+ def self.consume_defaults(autoscale_dir = AUTOSCALE_DIR)
33
+ defaults_file = File.join(autoscale_dir,"defaults.yml")
34
+ if File.exists?(defaults_file)
35
+ parsed = begin
36
+ Rivet::Log.debug("Consuming defaults from #{defaults_file}")
37
+ YAML.load(File.open(defaults_file))
38
+ rescue ArgumentError => e
39
+ Rivet::Log.fatal("Could not parse YAML from #{defaults_file}: #{e.message}")
40
+ end
41
+ parsed
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ # This loads the given definition from it's YML file, returns the hash or
48
+ # false if empty
49
+
50
+ def self.load_definition(name)
51
+ definition_dir = File.join(AUTOSCALE_DIR,name)
52
+ conf_file = File.join(definition_dir,"conf.yml")
53
+ if Dir.exists?(definition_dir) && File.exists?(conf_file)
54
+ Rivet::Log.debug("Loading definition for #{name} from #{conf_file}")
55
+ parsed = begin
56
+ YAML.load(File.open(conf_file))
57
+ rescue
58
+ Rivet::Log.fatal("Could not parse YAML from #{conf_file}: #{e.message}")
59
+ end
60
+ parsed ? parsed : { }
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+
@@ -0,0 +1,4 @@
1
+ module Rivet
2
+ VERSION = "1.0.0"
3
+ end
4
+
data/lib/rivet.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'aws-sdk'
2
+ require 'base64'
3
+ require 'digest/sha1'
4
+ require 'erb'
5
+ require 'json'
6
+ require 'logger'
7
+ require 'optparse'
8
+ require 'singleton'
9
+ require 'yaml'
10
+
11
+ require_relative 'rivet/deep_merge'
12
+ require_relative 'rivet/logger'
13
+ require_relative 'rivet/utils'
14
+ require_relative 'rivet/aws_utils'
15
+ require_relative 'rivet/launch_config'
16
+ require_relative 'rivet/autoscale'
17
+ require_relative 'rivet/bootstrap'
18
+ require_relative 'rivet/client'
data/rivet.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib/',__FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+ require 'rivet/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'rivet'
9
+ spec.version = Rivet::VERSION
10
+ spec.licenses = ['Apache2']
11
+ spec.authors = ['Brian Bianco']
12
+ spec.email = ['brian.bianco@gmail.com']
13
+ spec.homepage = 'http://www.github.com/brianbianco/rivet'
14
+ spec.summary = %q{A tool for managing autoscaling groups}
15
+ spec.description = %q{Rivet allows you to define autoscaling groups and launch configurations as YAML and can SYNC that to AWS}
16
+
17
+ spec.required_ruby_version = '>= 1.9.1'
18
+ spec.required_rubygems_version = '>= 1.3.6'
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+ spec.test_files = spec.files.grep(%r{^spec/})
22
+
23
+ spec.executables = %w(rivet)
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "aws-sdk", "~> 1.24.0"
27
+ spec.add_development_dependency "rake", ">= 10.1.0"
28
+ spec.add_development_dependency "rspec", "~> 2.14.1"
29
+ end
30
+
31
+
@@ -0,0 +1,69 @@
1
+ require_relative './rivet_spec_setup'
2
+
3
+ include SpecHelpers
4
+
5
+ describe 'rivet bootstrap' do
6
+ let (:bootstrap) { Rivet::Bootstrap.new(SpecHelpers::AUTOSCALE_DEF['bootstrap']) }
7
+ let (:bootstrap_def) { SpecHelpers::AUTOSCALE_DEF['bootstrap'] }
8
+
9
+ tempdir_context 'with all necessary files in place' do
10
+ before do
11
+
12
+
13
+ validator_file = File.join(
14
+ bootstrap_def['config_dir'],
15
+ "#{bootstrap_def['environment']}-validator.pem")
16
+
17
+ template_dir = File.join(
18
+ bootstrap_def['config_dir'],
19
+ Rivet::Bootstrap::TEMPLATE_SUB_DIR)
20
+
21
+ template_file = File.join(template_dir,bootstrap_def['template'])
22
+
23
+ FileUtils.mkdir_p(bootstrap_def['config_dir'])
24
+ FileUtils.mkdir_p(template_dir)
25
+ File.open(template_file,'w') { |f| f.write(SpecHelpers::BOOTSTRAP_TEMPLATE) }
26
+ FileUtils.touch(validator_file)
27
+ end
28
+
29
+ describe "#user_data" do
30
+ it 'returns a string that contains the chef organization' do
31
+ org = bootstrap_def['organization']
32
+ bootstrap.user_data.should =~ /chef_server_url\s*.*#{org}.*/
33
+ end
34
+
35
+ it 'returns a string that contains the environment' do
36
+ env = bootstrap_def['env']
37
+ bootstrap.user_data.should =~ /environment\s*.*#{env}.*/
38
+ end
39
+
40
+ it 'returns a string that contains the run_list as json' do
41
+ run_list = { :run_list => bootstrap_def['run_list'].join(",") }.to_json
42
+ bootstrap.user_data.should =~ /#{run_list}/
43
+ end
44
+
45
+ it 'returns a string that contains each gem to install' do
46
+ bootstrap_def['gems'].each do |g|
47
+ if g.size > 1
48
+ gem_regexp = /gem\s*install\s*#{g[0]}.*#{g[1]}/
49
+ else
50
+ gem_regexp = /gem\s*install\s*#{g[0]}/
51
+ end
52
+ bootstrap.user_data.should =~ gem_regexp
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
@@ -0,0 +1,37 @@
1
+ require_relative './rivet_spec_setup'
2
+
3
+ include SpecHelpers
4
+
5
+ describe "rivet launch config" do
6
+ let (:launch_config) { Rivet::LaunchConfig.new(SpecHelpers::AUTOSCALE_DEF) }
7
+
8
+ context "with a valid autoscale definition" do
9
+ before do
10
+ user_data_mock = double('user_data_mock')
11
+ user_data_mock.stub(:user_data).and_return("unit_test_user_data")
12
+ Rivet::Bootstrap.stub(:new).and_return(user_data_mock)
13
+ end
14
+
15
+ describe "#build_identity_string" do
16
+ it "should return a valid identity_string" do
17
+ launch_config.send(:build_identity_string).should == SpecHelpers::AUTOSCALE_IDENTITY_STRING
18
+ end
19
+ end
20
+
21
+ describe "#identity" do
22
+ it "should return a deterministic identity" do
23
+ launch_config.identity.should == "rivet_#{Digest::SHA1.hexdigest(SpecHelpers::AUTOSCALE_IDENTITY_STRING)}"
24
+ end
25
+ end
26
+
27
+ describe "#normalize_security_groups" do
28
+ it "returns a sorted array of groups" do
29
+ unsorted_groups = ['group3','group1','group2']
30
+ sorted_groups = unsorted_groups.sort
31
+ returned_groups = launch_config.send(:normalize_security_groups,unsorted_groups)
32
+ returned_groups.should == sorted_groups
33
+ end
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,62 @@
1
+ require 'rspec'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'pathname'
5
+ require 'base64'
6
+ require_relative '../lib/rivet'
7
+
8
+ Rivet::Log.level(Logger::FATAL)
9
+
10
+
11
+ module SpecHelpers
12
+
13
+
14
+ BOOTSTRAP_TEMPLATE = '<%= install_gems %>'\
15
+ '<%= config_content %>'\
16
+ '<%= first_boot %>'\
17
+ "\n"\
18
+ '<%= chef_command %>'
19
+
20
+
21
+ AUTOSCALE_DEF = {
22
+ 'min_size' => 1,
23
+ 'max_size' => 3,
24
+ 'region' => 'us-west-2',
25
+ 'availability_zones' => ['a','b','c'],
26
+ 'key_name' => 'UnitTests',
27
+ 'instance_type' => 'm1.large',
28
+ 'security_groups' => ['unit_tests1','unit_tests2'],
29
+ 'image_id' => 'ami-12345678',
30
+ 'bootstrap' => {
31
+ 'chef_organization' => 'unit_tests',
32
+ 'template' => 'default.erb',
33
+ 'config_dir' => 'unit_tests',
34
+ 'environment' => 'unit_tests',
35
+ 'gems' => [ ['gem1','0.0.1'],['gem2','0.0.2'] ],
36
+ 'run_list' => ['unit_tests']
37
+ }
38
+ }
39
+
40
+ AUTOSCALE_IDENTITY_STRING = "key_name#{Base64.encode64(AUTOSCALE_DEF['key_name'])}"\
41
+ "image_id#{Base64.encode64(AUTOSCALE_DEF['image_id'])}"\
42
+ "instance_type#{Base64.encode64(AUTOSCALE_DEF['instance_type'])}"\
43
+ "security_groups#{Base64.encode64(AUTOSCALE_DEF['security_groups'].join("\t"))}"\
44
+ "bootstrap#{Base64.encode64('unit_test_user_data')}"\
45
+
46
+ def tempdir_context(name, &block)
47
+ context name do
48
+ before do
49
+ @origin_dir = Dir.pwd
50
+ @temp_dir = ::Pathname.new(::File.expand_path(::Dir.mktmpdir))
51
+ Dir.chdir @temp_dir
52
+ end
53
+
54
+ after do
55
+ Dir.chdir @origin_dir
56
+ FileUtils.remove_entry(@temp_dir)
57
+ end
58
+
59
+ instance_eval &block
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,123 @@
1
+ require_relative './rivet_spec_setup'
2
+
3
+ include SpecHelpers
4
+
5
+ definition_name = "unit_test"
6
+ definition_dir = File.join(Rivet::Utils::AUTOSCALE_DIR,definition_name)
7
+ launch_config_params = ['ssh_key','instance_size','security_groups','ami','bootstrap']
8
+
9
+ defaults_hash = {
10
+ 'min_size' => 0,
11
+ 'max_size' => 0,
12
+ 'region' => 'us-west-2',
13
+ 'zones' => ['a','b','c'],
14
+ 'key_name' => 'unit_tests',
15
+ 'instance_type' => 'm1.large',
16
+ 'security_groups' => ['unit_tests'],
17
+ 'image_id' => 'ami-unit_tests',
18
+ 'bootstrap' => {
19
+ 'run_list' => ['role[unit_tests]']
20
+ }
21
+ }
22
+
23
+ unit_test_definition_hash = {
24
+ 'min_size' => 1,
25
+ 'max_size' => 5,
26
+ 'bootstrap' => {
27
+ 'run_list' => ['role[merging_test']
28
+ }
29
+ }
30
+
31
+ describe "rivet utils" do
32
+ tempdir_context "without an autoscaling directory" do
33
+ describe "ensure_minimum_setup" do
34
+ it "creates the autoscale directory if it doesn't exist" do
35
+ Rivet::Utils.ensure_minimum_setup
36
+ Dir.exists?(Rivet::Utils::AUTOSCALE_DIR).should be_true
37
+ end
38
+ end
39
+ end
40
+
41
+ tempdir_context "with an autoscaling directory" do
42
+ before do
43
+ FileUtils.mkdir_p(Rivet::Utils::AUTOSCALE_DIR)
44
+ end
45
+
46
+ describe "ensure_minimum_setup" do
47
+ it "should return true" do
48
+ Rivet::Utils.ensure_minimum_setup.should be_true
49
+ end
50
+ end
51
+
52
+ describe "consume_defaults" do
53
+ it "should return false" do
54
+ Rivet::Utils.consume_defaults.should be_false
55
+ end
56
+ end
57
+
58
+ describe "load_definition" do
59
+ it "should return false" do
60
+ Rivet::Utils.load_definition("unit_test").should be_false
61
+ end
62
+ end
63
+
64
+ describe "get_definition" do
65
+ it "should return false" do
66
+ Rivet::Utils.get_definition("unit_test")
67
+ end
68
+ end
69
+
70
+ context "and with a group directory" do
71
+ before do
72
+ FileUtils.mkdir_p definition_dir
73
+ end
74
+
75
+ describe "load_definition" do
76
+ it "should return false" do
77
+ Rivet::Utils.load_definition("unit_test").should be_false
78
+ end
79
+ end
80
+
81
+ context "and with a conf.yml" do
82
+ before do
83
+ FileUtils.mkdir_p definition_dir
84
+ File.open(File.join(definition_dir,"conf.yml"),'w') do |f|
85
+ f.write(unit_test_definition_hash.to_yaml)
86
+ end
87
+ end
88
+ describe "load_definition" do
89
+ it "returns a hash" do
90
+ loaded_def = Rivet::Utils.load_definition("unit_test")
91
+ unit_test_definition_hash.each_pair { |k,v| loaded_def.should include(k => v) }
92
+ end
93
+ end
94
+ context "and with a defaults.yml" do
95
+ before do
96
+ File.open(File.join(Rivet::Utils::AUTOSCALE_DIR,"defaults.yml"),'w') do |f|
97
+ f.write(defaults_hash.to_yaml)
98
+ end
99
+ end
100
+
101
+ describe "consume_defaults" do
102
+ it "consume defaults returns a hash" do
103
+ results = Rivet::Utils.consume_defaults
104
+ defaults_hash.each_pair { |k,v| results.should include(k => v) }
105
+ end
106
+ end
107
+
108
+ describe "get_definition" do
109
+ it "returns a merged hash" do
110
+ result = Rivet::Utils.get_definition(definition_name)
111
+ merged_hash = defaults_hash.merge(unit_test_definition_hash)
112
+ result.should == defaults_hash.merge(unit_test_definition_hash)
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+ end
122
+ end
123
+