delano-tryouts 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.txt ADDED
@@ -0,0 +1,6 @@
1
+ TRYOUTS, CHANGES
2
+
3
+
4
+ #### 0.4.0 (2009-06-05) ###############################
5
+
6
+ NOTE: Initial public release
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Solutious Inc, Delano Mandelbaum
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,122 @@
1
+ = Tryouts - v0.5 ALPHA
2
+
3
+ Tryouts is a high-level testing library for your command-line applications and Ruby codes.
4
+
5
+ *WORK IN PROGRESS*
6
+
7
+ 2009-05-24 - A BBQ started nearby as I was writing this documentation. It's incomplete and I'm sorry about that but I can tell you that this BBQ smells really, really good.
8
+
9
+
10
+ == Terminology
11
+
12
+ Tryouts is a bit different than other testing libraries. Test definitions are organized in a similar way as Shoulda tests (although the keywords in the syntax are different).
13
+
14
+ * Tryout: a set of drills (like basketball tryouts)
15
+ * Drill: a test.
16
+ * Dream: the expected outcome of a drill.
17
+
18
+
19
+ == Testing a command-line application (a CLI)
20
+
21
+ Tryouts tests command-line applications by comparing expected output with the actual output. Let's say we have an executable called mockout and we want to test the following commands:
22
+
23
+ $ bin/executable
24
+ $ bin/executable -f yaml
25
+
26
+ The tryout definition would look like this:
27
+
28
+ command :executable, "path/2/executable"
29
+
30
+ tryout "Common Usage", :cli do
31
+ drill "No Command"
32
+ drill "YAML Output", :f, "yaml"
33
+ end
34
+
35
+ And the expected output would be defined like this:
36
+
37
+ dream "No Command" do
38
+ output inline(%Q{
39
+ Date: 2009-02-16
40
+ Players: d-bam, alberta, birds, condor man
41
+ Owners: greg, rupaul, telly, prince kinko
42
+ })
43
+ end
44
+ dream "YAML Output" do
45
+ format :yaml
46
+ output ({
47
+ "Date" => "2009-02-16",
48
+ "Players" => ["d-bam", "alberta", "birds", "condor man"],
49
+ "Owners" => ["greg", "rupaul", "telly", "prince kinko"]
50
+ })
51
+ end
52
+
53
+ == Testing Ruby codes (an API)
54
+
55
+ Tryouts employs the same approach for testing Ruby codes. The return value of the drill block is compared to the expectation defined by the dream. Here is an example of including dreams inside the tryout definition.
56
+
57
+ library :caesars, LIBRARY_PATH
58
+
59
+ tryout "Common Usage", :api do
60
+ dream "Some Maths", 3
61
+ drill "Some Maths" do
62
+ 12 / 4
63
+ end
64
+
65
+ dream "A Block", Proc, :class # Test the class type instead of the value
66
+ drill "A Block" do
67
+ Proc.new do
68
+ :anything
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ == ALPHA Notice
75
+
76
+ This library is very new (est. 2009-05-19) and has not been vetted by the scrutiny of time. In particular you can expect:
77
+
78
+ * Ugly/awkward output from the tryouts executable. I wrote the core functionality that I needed to start writing tryouts for my other projects. I haven't spent very much time on reporting yet. However, this will change!
79
+ * The test definition syntax may change in future releases.
80
+ * Unexpected errors.
81
+
82
+ == 3 Ways to define tryouts
83
+ There are three ways to define an instance of this class:
84
+ * In +_tryouts.rb+ files using the DSL syntax. One file per Tryouts object.
85
+ * See: http://github.com/delano/tryouts/blob/master/tryouts/mockoutcli_tryouts.rb
86
+ * In standalone ruby files using a hybrid DSL/OO syntax. Supports multiple
87
+ Tryouts objects per file.
88
+ * See: http://github.com/delano/tryouts/blob/master/tryouts/standalone_test.rb
89
+ * With regular object-oriented syntax.
90
+ * See: http://tryouts.rubyforge.org/
91
+
92
+ == On Threads
93
+
94
+ Tryouts does some funky stuff to make it simple to write tests. This "funky
95
+ stuff" means that this library is *not thread-safe at definition-time*. However, once
96
+ all tryouts files are parsed (or in OO-syntax, once all objects are created), this
97
+ class should be *thread-safe at drill-time*.
98
+
99
+ == More Info
100
+
101
+ * Check out the sourcecodes[http://github.com/delano/tryouts]
102
+ * Read the rdocs[http://tryouts.rubyforge.org/]
103
+ * About Solutious[http://solutious.com/about/]
104
+
105
+ == Thanks
106
+
107
+ * Everyone at Utrecht.rb
108
+
109
+ == Credits
110
+
111
+ * Delano (@solutious.com)
112
+
113
+ == Related Projects
114
+
115
+ * Context[http://github.com/jeremymcanally/context/tree/master]
116
+ * Testy[http://github.com/ahoward/testy/tree/master]
117
+ * Expectations[http://expectations.rubyforge.org/]
118
+ * Zebra[http://github.com/giraffesoft/zebra/tree/master]
119
+
120
+ == License
121
+
122
+ See: LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1,80 @@
1
+ require 'rubygems'
2
+ require 'rake/clean'
3
+ require 'rake/gempackagetask'
4
+ require 'hanna/rdoctask'
5
+ require 'fileutils'
6
+ include FileUtils
7
+
8
+ task :default => :package
9
+
10
+ # CONFIG =============================================================
11
+
12
+ # Change the following according to your needs
13
+ README = "README.rdoc"
14
+ CHANGES = "CHANGES.txt"
15
+ LICENSE = "LICENSE.txt"
16
+
17
+ # Files and directories to be deleted when you run "rake clean"
18
+ CLEAN.include [ 'pkg', '*.gem', '.config', 'doc']
19
+
20
+ # Virginia assumes your project and gemspec have the same name
21
+ name = (Dir.glob('*.gemspec') || ['tryouts']).first.split('.').first
22
+ load "#{name}.gemspec"
23
+ version = @spec.version
24
+
25
+ # That's it! The following defaults should allow you to get started
26
+ # on other things.
27
+
28
+
29
+ # TESTS/SPECS =========================================================
30
+
31
+
32
+
33
+ # INSTALL =============================================================
34
+
35
+ Rake::GemPackageTask.new(@spec) do |p|
36
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
37
+ end
38
+
39
+ task :release => [ :rdoc, :package ]
40
+ task :install => [ :rdoc, :package ] do
41
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
42
+ end
43
+ task :uninstall => [ :clean ] do
44
+ sh %{sudo gem uninstall #{name}}
45
+ end
46
+
47
+
48
+ # RUBYFORGE RELEASE / PUBLISH TASKS ==================================
49
+
50
+ if @spec.rubyforge_project
51
+ desc 'Publish website to rubyforge'
52
+ task 'publish:rdoc' => 'doc/index.html' do
53
+ sh "scp -rp doc/* rubyforge.org:/var/www/gforge-projects/#{name}/"
54
+ end
55
+
56
+ desc 'Public release to rubyforge'
57
+ task 'publish:gem' => [:package] do |t|
58
+ sh <<-end
59
+ rubyforge add_release -o Any -a #{CHANGES} -f -n #{README} #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.gem &&
60
+ rubyforge add_file -o Any -a #{CHANGES} -f -n #{README} #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.tgz
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+
67
+ # RUBY DOCS TASK ==================================
68
+
69
+ Rake::RDocTask.new do |t|
70
+ t.rdoc_dir = 'doc'
71
+ t.title = @spec.summary
72
+ t.options << '--line-numbers' << '-A cattr_accessor=object'
73
+ t.options << '--charset' << 'utf-8'
74
+ t.rdoc_files.include(LICENSE)
75
+ t.rdoc_files.include(README)
76
+ t.rdoc_files.include(CHANGES)
77
+ t.rdoc_files.include('bin/tryouts')
78
+ t.rdoc_files.include('lib/**/*.rb')
79
+ end
80
+
data/bin/mockout ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # = Mockout
4
+ #
5
+ # This mock is used to generate test output for Tryouts itself.
6
+ # It is otherwise uninteresting!
7
+ #
8
+
9
+ require 'rubygems'
10
+ require 'drydock'
11
+ require 'yaml'
12
+
13
+ begin; require 'json'; rescue LoadError; end # json may not be installed
14
+
15
+ module DrillCLI
16
+ extend Drydock
17
+
18
+ global :f, :format, String, "One of: json, yaml, string (default)"
19
+
20
+
21
+ default :info
22
+ debug :on
23
+
24
+ data = {
25
+ "Date" => "2009-02-16",
26
+ "Owners" => ["greg", "rupaul", "telly", "prince kinko"],
27
+ "Players" => ["d-bam", "alberta", "birds", "condor man"]
28
+ }
29
+
30
+ option :e, :echo, "Echo the arguments"
31
+ command :info do |obj|
32
+ format = obj.global.format.nil? ? nil : "to_#{obj.global.format}"
33
+ format = nil if obj.global.format == 'string'
34
+ if obj.option.echo
35
+ puts obj.argv
36
+ else
37
+ if format.nil?
38
+ data.keys.sort.each do |n|
39
+ val = data[n]
40
+ val = val.join(', ') if val.is_a?(Array)
41
+ puts "%9s %44s" % ["#{n}:", val]
42
+ end
43
+ else
44
+ puts data.send(format)
45
+ end
46
+ end
47
+ end
48
+
49
+ end
data/bin/tryouts ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/ruby
2
+
3
+ TRYOUTS_HOME = File.expand_path(File.join(File.dirname(__FILE__), '..'))
4
+
5
+ local_libs = %w{tryouts net-scp amazon-ec2 aws-s3 caesars drydock rye storable sysinfo annoy}
6
+ local_libs.each { |dir| $:.unshift File.join(TRYOUTS_HOME, '..', dir, 'lib') }
7
+
8
+ require 'drydock'
9
+ require 'tryouts'
10
+ require 'tryouts/cli'
11
+
12
+ # = TryoutsCLI
13
+ #
14
+ # This is the Drydock definition for the bin/tryouts executable
15
+ module TryoutsCLI
16
+ extend Drydock
17
+
18
+ debug :on
19
+ default :run, :with_args
20
+
21
+ global :v, :verbose, "Increase output" do
22
+ @verbose ||= 0
23
+ @verbose += 1
24
+ end
25
+
26
+ about "Run tryouts from current working directory"
27
+ argv :files
28
+ command :run => Tryouts::CLI::Run
29
+
30
+ about "Show dreams available from the current working directory"
31
+ argv :files
32
+ command :dreams => Tryouts::CLI::Run
33
+
34
+ about "Show tryouts available from the current working directory"
35
+ argv :files
36
+ command :list => Tryouts::CLI::Run
37
+
38
+ end
@@ -0,0 +1,74 @@
1
+
2
+ class Tryouts; module CLI
3
+
4
+ # = Run
5
+ #
6
+ # The logic bin/tryouts uses for running tryouts.
7
+ class Run < Drydock::Command
8
+
9
+ def init
10
+ @tryouts_globs = [GYMNASIUM_GLOB, File.join(Dir.pwd, '*_tryouts.rb')]
11
+ end
12
+
13
+ def dreams
14
+ load_available_tryouts_files
15
+ if @global.verbose > 0
16
+ puts Tryouts.dreams.to_yaml
17
+ else
18
+ Tryouts.dreams.each_pair do |n,dreams|
19
+ puts n
20
+ dreams.each_pair do |n, dream|
21
+ puts " " << n
22
+ dream.each_pair do |n, drill|
23
+ puts " " << n
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def run
31
+ load_available_tryouts_files
32
+ Tryouts.run
33
+ end
34
+
35
+ def list
36
+ load_available_tryouts_files
37
+ if @global.verbose > 0
38
+ puts Tryouts.instances.to_yaml
39
+ else
40
+ Tryouts.instances.each_pair do |n,tryouts|
41
+ puts n
42
+ tryouts.tryouts.each do |tryout|
43
+ puts " " << tryout.name
44
+ tryout.drills.each do |drill|
45
+ puts " " << drill.name
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+ def load_available_tryouts_files
54
+ @tryouts_files = []
55
+
56
+ if @argv.files
57
+ @argv.files.each do |file|
58
+ file = File.join(file, '**', '*_tryouts.rb') if File.directory?(file)
59
+ @tryouts_files += Dir.glob file
60
+ end
61
+ else
62
+ @tryouts_globs.each do |glob|
63
+ @tryouts_files += Dir.glob glob
64
+ end
65
+ end
66
+
67
+ @tryouts_files.uniq!
68
+
69
+ puts "FOUND:", @tryouts_files if @global.verbose > 0
70
+
71
+ @tryouts_files.each { |file| Tryouts.parse_file file }
72
+ end
73
+ end
74
+ end; end
@@ -0,0 +1,15 @@
1
+
2
+ class Tryouts
3
+
4
+ # = CLI
5
+ #
6
+ # A namespace for the tryouts executable (bin/tryouts) logic.
7
+ #
8
+ # This convention comes from Drydock (http://github.com/delano/drydock)
9
+ #
10
+ module CLI;end
11
+
12
+ end
13
+
14
+ require 'tryouts/cli/run'
15
+
@@ -0,0 +1,109 @@
1
+
2
+ class Tryouts::Drill
3
+ # = Response
4
+ #
5
+ # A generic base class for Dream and Reality
6
+ #
7
+ class Response
8
+ attr_accessor :output, :format, :rcode, :emsg, :backtrace
9
+ def initialize(output=nil, format=nil, rcode=0)
10
+ @output, @format, @rcode = output, format, (rcode || 0)
11
+ @format ||= :string
12
+ @output ||= []
13
+ normalize!
14
+ end
15
+
16
+ def ==(other)
17
+ return false if other.nil?
18
+ @rcode == other.rcode &&
19
+ @emsg == other.emsg &&
20
+ compare_output(other)
21
+ end
22
+
23
+ def rcode(val=nil); @rcode = val unless val.nil?; normalize!; @rcode; end
24
+ def output(val=nil); @output = val unless val.nil?; normalize!; @output; end
25
+ def emsg(val=nil); @emsg = val unless val.nil?; normalize!; @emsg; end
26
+ def format(val=nil); @format = val unless val.nil?; normalize!; @format; end
27
+
28
+ def output=(val); @output = val; normalize!; @output; end
29
+ def rcode=(val); @rcode = val; normalize!; @rcode; end
30
+ def format=(val); @format = val; normalize!; @format; end
31
+ def emsg=(val); @emsg = val; normalize!; @emsg; end
32
+
33
+ # Enforce the following restrictions on the data fields:
34
+ # * +rcode+ is an Integer
35
+ # * +format+ is a Symbol
36
+ # This method is called automatically any time a field is updated.
37
+ def normalize!
38
+ @rcode = @rcode.to_i if @rcode.is_a?(String)
39
+ @format = @format.to_sym if @format.is_a?(String)
40
+ end
41
+ def compare_output(other)
42
+ return true if @output == other.output
43
+
44
+ if @format == :class
45
+ if @output.is_a?(Class)
46
+ klass, payload = @output, other.output
47
+ elsif other.output.is_a?(Class)
48
+ klass, payload = other.output, @output
49
+ end
50
+ return payload.is_a?(klass)
51
+ end
52
+
53
+ if @output.kind_of?(Array) && other.kind_of?(Array)
54
+ return false unless @output.size == other.output.size
55
+
56
+ if @output.first.is_a?(Regexp)
57
+ expressions, strings = @output, other.output
58
+ elsif other.output.first.is_a?(Regexp)
59
+ expressions, strings = other.output, @output
60
+ end
61
+
62
+ if !expressions.nil? && !strings.nil?
63
+ expressions.each_with_index do |regex, index|
64
+ return false unless strings[index] =~ regex
65
+ end
66
+ return true
67
+ end
68
+ end
69
+
70
+ false
71
+ end
72
+
73
+ end
74
+
75
+
76
+ # = Dream
77
+ #
78
+ # Contains the expected response of a Drill
79
+ #
80
+ class Dream < Tryouts::Drill::Response
81
+
82
+ def self.from_block(definition)
83
+ d = Tryouts::Drill::Dream.new
84
+ d.from_block definition
85
+ d
86
+ end
87
+
88
+ def from_block(definition)
89
+ instance_eval &definition
90
+ self.normalize!
91
+ self
92
+ end
93
+
94
+ # Takes a String +val+ and splits the lines into an Array. Each line
95
+ # has
96
+ def inline(val=nil)
97
+ lines = (val.split($/) || []).collect { |line| line.strip }
98
+ lines.reject! { |line| line == "" }
99
+ lines
100
+ end
101
+ end
102
+
103
+ # = Reality
104
+ #
105
+ # Contains the actual response of a Drill
106
+ #
107
+ class Reality < Tryouts::Drill::Response; end
108
+
109
+ end
@@ -0,0 +1,49 @@
1
+
2
+
3
+ class Tryouts; class Drill; module Sergeant
4
+
5
+ # = CLI
6
+ #
7
+ # The sergeant responsible for running command-line interface drills.
8
+ #
9
+ class CLI
10
+
11
+ attr_reader :rbox
12
+
13
+ # An Array of arguments to be sent to +rbox.send(*rbox_args)+
14
+ attr_accessor :rbox_args
15
+
16
+ def initialize(*args)
17
+ @rbox_args = args
18
+ @rbox = Rye::Box.new
19
+ end
20
+
21
+ # NOTE: Context is ignored for this Sergeant.
22
+ def run(block, context=nil, &inline)
23
+ # A Proc object takes precedence over an inline block.
24
+ runtime = (block.nil? ? inline : block)
25
+ response = Tryouts::Drill::Reality.new
26
+ begin
27
+ if runtime.nil?
28
+ ret = @rbox.send *rbox_args
29
+ else
30
+ ret = @rbox.instance_eval &runtime
31
+ end
32
+ response.rcode = ret.exit_code
33
+ response.output = ret.stdout.size == 1 ? ret.stdout.first : Array.new(ret.stdout) # Cast the Rye::Rap object
34
+ response.emsg = ret.stderr unless ret.stderr.empty?
35
+ rescue Rye::CommandNotFound => ex
36
+ response.rcode = -2
37
+ response.emsg = "[#{@rbox.host}] Command not found: #{ex.message}"
38
+ response.backtrace = ex.backtrace
39
+ rescue Rye::CommandError => ex
40
+ response.rcode = ex.exit_code
41
+ response.output = ex.stdout
42
+ response.emsg = ex.stderr
43
+ end
44
+ response
45
+ end
46
+
47
+ end
48
+
49
+ end; end; end
@@ -0,0 +1,103 @@
1
+
2
+
3
+ class Tryouts
4
+
5
+ # = Drill
6
+ #
7
+ # This class represents a drill. A drill is single test.
8
+ #
9
+ class Drill
10
+
11
+ require 'tryouts/drill/response'
12
+ require 'tryouts/drill/sergeant/cli'
13
+ require 'tryouts/drill/sergeant/api'
14
+
15
+ class NoSergeant < Tryouts::Exception; end
16
+
17
+ # A symbol specifying the drill type. One of: :cli, :api
18
+ attr_reader :dtype
19
+ # The name of the drill. This should match the name used in the dreams file.
20
+ attr_reader :name
21
+ # A Proc object which contains the drill logic.
22
+ attr_reader :drill
23
+
24
+ # A Sergeant object which executes the drill
25
+ attr_reader :sergeant
26
+ # A Dream object (the expected output of the test)
27
+ attr_reader :dream
28
+ # A Reality object (the actual output of the test)
29
+ attr_reader :reality
30
+
31
+ def initialize(name, dtype, *drill_args, &drill)
32
+ @name, @dtype, @drill = name, dtype, drill
33
+ @sergeant = hire_sergeant *drill_args
34
+ # For CLI drills, a block takes precedence over inline args.
35
+ drill_args = [] if dtype == :cli && drill.is_a?(Proc)
36
+ end
37
+
38
+ def hire_sergeant(*drill_args)
39
+ if @dtype == :cli
40
+ Tryouts::Drill::Sergeant::CLI.new(*drill_args)
41
+ elsif @dtype == :api
42
+ Tryouts::Drill::Sergeant::API.new(*drill_args)
43
+ else
44
+ raise NoSergeant, "What is #{@dtype}?"
45
+ end
46
+ end
47
+
48
+ def run(context=nil)
49
+ context ||= Class.new
50
+ begin
51
+ print Tryouts::DRILL_MSG % @name
52
+ @reality = @sergeant.run @drill, context
53
+ process_reality
54
+ rescue => ex
55
+ @reality = Tryouts::Drill::Reality.new
56
+ @reality.rcode = -2
57
+ @reality.emsg, @reality.backtrace = ex.message, ex.backtrace
58
+ end
59
+ note = @dream ? discrepency.join(', ') : 'nodream'
60
+ puts self.success? ? "PASS" : "FAIL (#{note})"
61
+ self.success?
62
+ end
63
+
64
+ def success?
65
+ @dream == @reality
66
+ end
67
+
68
+ def discrepency
69
+ diffs = []
70
+ if @dream
71
+ diffs << "rcode" if @dream.rcode != @reality.rcode
72
+ diffs << "output" if @dream.output != @reality.output
73
+ diffs << "emsg" if @dream.emsg != @reality.emsg
74
+ end
75
+ diffs
76
+ end
77
+
78
+ def add_dream(d)
79
+ @dream = d if d.is_a?(Tryouts::Drill::Dream)
80
+ end
81
+
82
+ private
83
+ # Use the :format provided in the dream to convert the output from reality
84
+ def process_reality
85
+ @reality.normalize!
86
+ return unless @dream && @dream.format
87
+ if @dream.format.to_s == "yaml"
88
+ @reality.output = YAML.load(@reality.output.join("\n"))
89
+ elsif @dream.format.to_s == "json"
90
+ @reality.output = JSON.load(@reality.output.join("\n"))
91
+ end
92
+
93
+ if @reality.output.is_a?(Array)
94
+ # Remove new lines from String output
95
+ @reality.output = @reality.output.collect do |line|
96
+ line.is_a?(String) ? line.strip : line
97
+ end
98
+ end
99
+
100
+ #p [:process, @name, @dream.format, @reality.output]
101
+ end
102
+
103
+ end; end
@@ -0,0 +1,25 @@
1
+
2
+ class Hash
3
+
4
+ # A depth-first look to find the deepest point in the Hash.
5
+ # The top level Hash is counted in the total so the final
6
+ # number is the depth of its children + 1. An example:
7
+ #
8
+ # ahash = { :level1 => { :level2 => {} } }
9
+ # ahash.deepest_point # => 3
10
+ #
11
+ def deepest_point(h=self, steps=0)
12
+ if h.is_a?(Hash)
13
+ steps += 1
14
+ h.each_pair do |n,possible_h|
15
+ ret = deepest_point(possible_h, steps)
16
+ steps = ret if steps < ret
17
+ end
18
+ else
19
+ return 0
20
+ end
21
+ steps
22
+ end
23
+
24
+ end
25
+
@@ -0,0 +1,2 @@
1
+
2
+ require "tryouts/mixins/hash"