rivet 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+