Skip to content

Commit 56a8f22

Browse files
committedJun 5, 2013
tests
1 parent 9e41819 commit 56a8f22

19 files changed

+426
-60
lines changed
 

‎Gemfile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
source 'https://rubygems.org'
2+
3+
gemspec
4+
5+
gem 'rake'
6+
gem 'mocha', :require => false

‎Gemfile.lock

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
PATH
2+
remote: .
3+
specs:
4+
hull (0.1.0)
5+
colored
6+
net-ssh
7+
8+
GEM
9+
remote: https://rubygems.org/
10+
specs:
11+
colored (1.2)
12+
metaclass (0.0.1)
13+
mocha (0.13.1)
14+
metaclass (~> 0.0.1)
15+
net-ssh (2.6.7)
16+
rake (10.0.4)
17+
18+
PLATFORMS
19+
ruby
20+
21+
DEPENDENCIES
22+
hull!
23+
mocha
24+
rake

‎Rakefile

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env rake
2+
require "bundler/gem_tasks"
3+
require 'rake/testtask'
4+
5+
Rake::TestTask.new do |t|
6+
t.pattern = 'test/**/*_test.rb'
7+
end
8+
9+
task :default => :test

‎bin/hull

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ require Dir.pwd + '/config/hull'
1212

1313

1414
if arg_command == 'apply'
15-
apply(arg_node, arg_package, :install)
15+
execute(arg_node, arg_package, :apply)
16+
elsif arg_command == 'remove'
17+
execute(arg_node, arg_package, :remove)
1618
else
17-
demonstrate(arg_node, arg_package, :install)
19+
demonstrate(arg_node, arg_package, :apply)
1820
end

‎lib/hull.rb

+2
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ module Hull; end
66
require_relative "./hull/package_index"
77
require_relative "./hull/node"
88
require_relative "./hull/runner"
9+
require_relative "./hull/resolver"
10+
require_relative "./hull/execution_context"
911
require_relative "./hull/dsl"

‎lib/hull/dsl.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ def package(name, &definition)
77
Hull::PackageIndex.default.add(pkg)
88
end
99

10-
def apply(node_name, pkg_name, command)
10+
def execute(node_name, pkg_name, command)
1111
node = Hull::Node.find(node_name)
1212
pkg = Hull::PackageIndex.default.get(pkg_name)
13-
Hull::Runner.new(node, pkg).apply(command)
13+
Hull::Runner.new(node, pkg).execute(command)
1414
end
1515

1616
def demonstrate(node_name, pkg_name, command)

‎lib/hull/execution_context.rb

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class Hull::ExecutionContext
2+
def initialize(node)
3+
@node = node
4+
end
5+
6+
def apply(blk)
7+
instance_eval(&blk)
8+
end
9+
10+
def run(cmd)
11+
@node.execute(cmd)
12+
end
13+
14+
def trigger(action_ref, *args)
15+
pkg_name, action_name = *action_ref.split(':', 2)
16+
pkg = Hull::PackageIndex.default.get(pkg_name)
17+
action = pkg.actions[action_name]
18+
raise "Action #{action_ref} could not be found." unless action
19+
instance_exec(*args, &action)
20+
end
21+
22+
def binary_exists?(binary)
23+
run("which #{binary}") =~ /\/#{binary}/
24+
end
25+
end
26+
27+
class Hull::MockExecutionContext < Hull::ExecutionContext
28+
def run(cmd)
29+
@node.log 'mock-execute', cmd
30+
end
31+
end

‎lib/hull/node.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require 'net/ssh'
22

33
class Hull::Node
4-
attr_reader :name
4+
attr_reader :name, :host
55

66
def self.find(name)
77
@nodes[name]
@@ -21,12 +21,15 @@ def initialize(name, host)
2121

2222
def execute(cmd)
2323
log('execute', cmd.cyan)
24+
output = ""
2425
connection.exec! cmd do |channel, stream, data|
26+
output += data if stream == :stdout
2527
data.split("\n").each do |line|
2628
msg = stream == :stdout ? line.green : line.red
2729
log(stream, msg)
2830
end
2931
end
32+
output
3033
end
3134

3235
def log(context, msg)

‎lib/hull/package.rb

+10-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ def initialize(name)
99
@remove = nil
1010
end
1111

12-
def depends_on(pkg_name)
13-
@dependancies << pkg_name
12+
def depends_on(*pkg_names)
13+
pkg_names.each do |pkg_name|
14+
@dependancies << pkg_name
15+
end
1416
end
1517

16-
def install(&definition)
17-
@commands[:install] = definition
18+
def validate(&definition)
19+
@commands[:validate] = definition
20+
end
21+
22+
def apply(&definition)
23+
@commands[:apply] = definition
1824
end
1925

2026
def remove(&definition)

‎lib/hull/package_index.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,22 @@ def add(pkg)
1616

1717
def get(pkg_name)
1818
pkg = @packages[pkg_name]
19-
raise "Missing Package #{pkg_name}" if pkg.nil?
19+
raise MissingPackageError.new(index_name, pkg_name) if pkg.nil?
2020
pkg
2121
end
22+
23+
def clear!
24+
@packages = {}
25+
end
26+
27+
class MissingPackageError < StandardError
28+
def initialize(index_name, package_name)
29+
@index_name = index_name
30+
@package_name = package_name
31+
end
32+
33+
def message
34+
"package #{@package_name} could not be found in the index #{@index_name}"
35+
end
36+
end
2237
end

‎lib/hull/packages/apt.rb

+38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
module Hull::DSL
2+
def apt_package(pkg_name, apt_name=pkg_name, &blk)
3+
package pkg_name do
4+
depends_on 'apt'
5+
validate { trigger 'apt:exists', apt_name }
6+
apply { trigger 'apt:install', apt_name }
7+
remove { trigger 'apt:remove', apt_name }
8+
instance_eval(&blk) if blk
9+
end
10+
end
11+
212
package 'apt' do
313
action 'install' do |package_name|
414
run "DEBIAN_FRONTEND=noninteractive apt-get install -y -q #{package_name}"
@@ -7,5 +17,33 @@ module Hull::DSL
717
action 'remove' do |package_name|
818
run "DEBIAN_FRONTEND=noninteractive apt-get remove -y -q #{package_name}"
919
end
20+
21+
action 'ppa' do |repo|
22+
run "DEBIAN_FRONTEND=noninteractive add-apt-repository #{repo} -y"
23+
end
24+
25+
action 'update' do
26+
run "DEBIAN_FRONTEND=noninteractive apt-get update -y -qq"
27+
end
28+
29+
action 'exists' do |package_name|
30+
run("dpkg -s #{package_name} 2>&1 | grep Status") =~ /Status: install ok installed/
31+
end
32+
33+
validate do
34+
trigger('apt:exists', 'python-software-properties') &&
35+
trigger('apt:exists', 'software-properties-common')
36+
end
37+
38+
apply do
39+
trigger 'apt:install', 'python-software-properties'
40+
trigger 'apt:install', 'software-properties-common'
41+
trigger 'apt:update'
42+
end
43+
44+
remove do
45+
trigger 'apt:remove', 'python-software-properties'
46+
trigger 'apt:remove', 'software-properties-common'
47+
end
1048
end
1149
end

‎lib/hull/packages/gem.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Hull::DSL
2+
def gem_package(pkg_name, gem_name=pkg_name)
3+
package pkg_name do
4+
depends_on 'gem' # should depend on Ruby but which???
5+
validate { trigger 'gem:exists', gem_name }
6+
apply { trigger 'gem:install', gem_name }
7+
remove { trigger 'gem:remove', gem_name }
8+
end
9+
end
10+
11+
package 'gem' do
12+
action 'exists' do |gem_name|
13+
run("gem list -i #{gem_name}") =~ /true/
14+
end
15+
16+
action 'install' do |gem_name|
17+
run "gem install #{gem_name} --no-ri --no-rdoc"
18+
end
19+
20+
action 'remove' do |gem_name|
21+
run "gem uninstall #{gem_name} -x -a"
22+
end
23+
end
24+
end

‎lib/hull/resolver.rb

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class Hull::Resolver
2+
attr_reader :packages, :tree
3+
def initialize(package)
4+
@package = package
5+
@last_seen = package
6+
@tree = [@package]
7+
@packages = []
8+
end
9+
10+
def resolve
11+
dependancies = @package.dependancies.map { |d| Hull::PackageIndex.default.get(d) }
12+
begin
13+
@tree += dependancies.map {|d| Hull::Resolver.new(d).resolve.tree }
14+
rescue SystemStackError
15+
raise CircularDependancyError.new
16+
end
17+
@packages = @tree.flatten
18+
@packages.reverse!
19+
@packages.uniq!
20+
self
21+
end
22+
23+
class CircularDependancyError < StandardError
24+
end
25+
end

‎lib/hull/runner.rb

+27-50
Original file line numberDiff line numberDiff line change
@@ -11,65 +11,42 @@ def packages
1111
resolver.packages
1212
end
1313

14-
def apply(command_name)
14+
def execute(command_name)
15+
@node.log command_name, packages.map(&:name).join(', ').yellow
1516
packages.each do |pkg|
16-
next unless pkg.provides_command?(command_name)
17-
@node.log pkg.name, command_name.to_s.yellow
18-
context = @perform ? Hull::ExecutionContext.new(@node) : Hull::MockExecutionContext.new(@node)
19-
cmd = pkg.command(command_name)
20-
context.apply(cmd)
17+
next unless should_run?(pkg, command_name)
18+
exec(pkg, command_name)
19+
validate!(pkg) if command_name == :apply
2120
end
2221
end
2322

24-
def demonstrate(command_name)
25-
@perform = false
26-
apply(command_name)
27-
@perform = true
28-
end
29-
end
30-
31-
32-
class Hull::ExecutionContext
33-
def initialize(node)
34-
@node = node
35-
end
36-
37-
def apply(blk)
38-
instance_eval(&blk)
23+
def should_run?(pkg, command_name)
24+
return false unless pkg.provides_command?(command_name)
25+
return true unless @perform
26+
return true unless command_name == :apply || command_name == :remove
27+
return true unless pkg.provides_command?(:validate)
28+
is_present = exec(pkg, :validate)
29+
return !is_present if command_name == :apply
30+
return is_present
3931
end
4032

41-
def run(cmd)
42-
@node.execute(cmd)
33+
def validate!(pkg)
34+
return true unless @perform
35+
return unless pkg.provides_command?(:validate)
36+
return if exec(pkg, :validate)
37+
raise "Package #{pkg.name} failed validation"
4338
end
4439

45-
def trigger(action, *args)
46-
pkg_name, action_name = *action.split(':', 2)
47-
pkg = Hull::PackageIndex.default.get(pkg_name)
48-
action = pkg.actions[action_name]
49-
instance_exec(*args, &action)
50-
end
51-
end
52-
53-
class Hull::MockExecutionContext < Hull::ExecutionContext
54-
def run(cmd)
55-
@node.log 'mock-execute', cmd
56-
end
57-
end
58-
59-
60-
class Hull::Resolver
61-
attr_reader :packages
62-
def initialize(package)
63-
@package = package
64-
@packages = [@package]
40+
def demonstrate(command_name)
41+
@perform = false
42+
apply(command_name)
43+
@perform = true
6544
end
6645

67-
def resolve
68-
dependancies = @package.dependancies.map { |d| Hull::PackageIndex.default.get(d) }
69-
@packages += dependancies.map {|d| Hull::Resolver.new(d).resolve.packages }
70-
@packages.flatten!
71-
@packages.reverse!
72-
@packages.uniq!
73-
self
46+
def exec(pkg, command_name)
47+
@node.log pkg.name, command_name.to_s.yellow
48+
context = @perform ? Hull::ExecutionContext.new(@node) : Hull::MockExecutionContext.new(@node)
49+
cmd = pkg.command(command_name)
50+
context.apply(cmd)
7451
end
7552
end

‎test/dsl_test.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require_relative 'test_helper'
2+
3+
describe Hull::DSL do
4+
describe "'package' command" do
5+
after :each do
6+
reset_package_index!
7+
end
8+
9+
it "adds a package to the index" do
10+
Hull::DSL.package('my-package') { nil }
11+
Hull::PackageIndex.default.get('my-package').must_be_instance_of Hull::Package
12+
end
13+
14+
it "creates a new package based on the supplied name" do
15+
Hull::DSL.package('my-package') { nil }
16+
package = Hull::PackageIndex.default.get('my-package')
17+
package.name.must_equal 'my-package'
18+
end
19+
20+
it "executes the given definition block in the package context" do
21+
Hull::DSL.package('my-package') { depends_on 'other-package' }
22+
package = Hull::PackageIndex.default.get('my-package')
23+
package.dependancies.must_equal ['other-package']
24+
end
25+
end
26+
27+
describe "'node' command" do
28+
it "creates a node and adds it to the index" do
29+
Hull::DSL.node('node-name', 'node-host')
30+
node = Hull::Node.find('node-name')
31+
node.name.must_equal 'node-name'
32+
node.host.must_equal 'node-host'
33+
end
34+
end
35+
end

‎test/package_index_test.rb

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require_relative 'test_helper'
2+
3+
describe Hull::PackageIndex do
4+
before :each do
5+
@package = Hull::Package.new('my-package')
6+
@default = Hull::PackageIndex.default
7+
end
8+
9+
after :each do
10+
reset_package_index!
11+
end
12+
13+
describe "default" do
14+
it "returns a package index singleton named default" do
15+
Hull::PackageIndex.default.index_name.must_equal 'default'
16+
end
17+
18+
it "allways returns the same package index" do
19+
Hull::PackageIndex.default.must_equal Hull::PackageIndex.default
20+
end
21+
end
22+
23+
describe "add" do
24+
it "adds a package to the index" do
25+
@default.add(@package)
26+
@default.get(@package.name).must_equal @package
27+
end
28+
end
29+
30+
describe 'get' do
31+
it "fetches a package by name" do
32+
@default.add(@package)
33+
@default.get(@package.name).must_equal @package
34+
end
35+
36+
it "throws an execption if the package doesn't exist" do
37+
assert_raises(Hull::PackageIndex::MissingPackageError) { @default.get(@package.name) }
38+
end
39+
end
40+
41+
describe "clear!" do
42+
it "wipes the index clean of packages" do
43+
@default.add(@package)
44+
@default.clear!
45+
assert_raises(Hull::PackageIndex::MissingPackageError) { @default.get(@package.name) }
46+
end
47+
end
48+
end

‎test/package_test.rb

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require_relative 'test_helper'
2+
3+
describe Hull::Package do
4+
before :each do
5+
@package = Hull::Package.new('my-package')
6+
end
7+
8+
describe "depends_on" do
9+
it "adds a dependancy" do
10+
@package.dependancies.must_equal []
11+
@package.depends_on('other-package')
12+
@package.dependancies.must_equal ['other-package']
13+
end
14+
15+
it "adds multiple dependancies at once" do
16+
@package.dependancies.must_equal []
17+
@package.depends_on('other-package', 'third-package')
18+
@package.dependancies.must_equal ['other-package', 'third-package']
19+
end
20+
end
21+
22+
describe "action" do
23+
it "allows defining actions with a name and a block" do
24+
@package.action('my-action') { 'foo' }
25+
@package.actions['my-action'].call.must_equal 'foo'
26+
end
27+
end
28+
29+
describe "commands" do
30+
it "can add an 'apply' command" do
31+
@package.apply { 'my-apply' }
32+
@package.command(:apply).call.must_equal 'my-apply'
33+
end
34+
35+
it "can add an 'remove' command" do
36+
@package.remove { 'my-remove' }
37+
@package.command(:remove).call.must_equal 'my-remove'
38+
end
39+
40+
it "can add an 'validate' command" do
41+
@package.validate { 'my-validate' }
42+
@package.command(:validate).call.must_equal 'my-validate'
43+
end
44+
45+
it 'knows if a command is provided' do
46+
@package.provides_command?(:apply).must_equal false
47+
@package.apply { 'my-apply' }
48+
@package.provides_command?(:apply).must_equal true
49+
end
50+
end
51+
end

‎test/resolver_test.rb

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
require_relative 'test_helper'
2+
3+
describe Hull::Resolver do
4+
before :each do
5+
@pkg_a = Hull::DSL.package('a') {}
6+
@pkg_b = Hull::DSL.package('b') {}
7+
@pkg_c = Hull::DSL.package('c') {}
8+
@pkg_d = Hull::DSL.package('d') {}
9+
end
10+
11+
after :each do
12+
reset_package_index!
13+
end
14+
15+
describe "resolving a tree of package dependancies" do
16+
it "resolves single packages to a single package" do
17+
resolver = Hull::Resolver.new(@pkg_a)
18+
resolver.resolve
19+
resolver.packages.must_equal [@pkg_a]
20+
end
21+
22+
it "resolves a chain of 2 packages to a list of 2 packages" do
23+
@pkg_a.depends_on(@pkg_b.name)
24+
resolver = Hull::Resolver.new(@pkg_a)
25+
resolver.resolve
26+
resolver.packages.must_equal [@pkg_b, @pkg_a]
27+
end
28+
29+
it "resolves a chain of 3 packages to a list of 3 packages" do
30+
@pkg_a.depends_on(@pkg_b.name)
31+
@pkg_b.depends_on(@pkg_c.name)
32+
resolver = Hull::Resolver.new(@pkg_a)
33+
resolver.resolve
34+
resolver.packages.must_equal [@pkg_c, @pkg_b, @pkg_a]
35+
end
36+
37+
it "resolves a tree of 3 packages to a list of 3 packages" do
38+
@pkg_a.depends_on(@pkg_b.name)
39+
@pkg_a.depends_on(@pkg_c.name)
40+
resolver = Hull::Resolver.new(@pkg_a)
41+
resolver.resolve
42+
resolver.packages.must_equal [@pkg_c, @pkg_b, @pkg_a]
43+
end
44+
45+
it "resolves a tree of 4 packages to a list of 4 packages" do
46+
@pkg_a.depends_on(@pkg_b.name)
47+
@pkg_a.depends_on(@pkg_c.name)
48+
@pkg_c.depends_on(@pkg_d.name)
49+
resolver = Hull::Resolver.new(@pkg_a)
50+
resolver.resolve
51+
resolver.packages.must_equal [@pkg_d, @pkg_c, @pkg_b, @pkg_a]
52+
end
53+
54+
it "errors out on circular dependancies" do
55+
@pkg_a.depends_on(@pkg_b.name)
56+
@pkg_b.depends_on(@pkg_a.name)
57+
resolver = Hull::Resolver.new(@pkg_a)
58+
assert_raises(Hull::Resolver::CircularDependancyError) { resolver.resolve }
59+
end
60+
end
61+
end

‎test/test_helper.rb

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
gem 'minitest'
2+
require 'minitest/autorun'
3+
require 'minitest/pride'
4+
require 'mocha/setup'
5+
require_relative '../lib/hull'
6+
7+
def reset_package_index!
8+
Hull::PackageIndex.default.clear!
9+
end

0 commit comments

Comments
 (0)
Please sign in to comment.