nestor 0.1.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.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +34 -0
  5. data/Rakefile +76 -0
  6. data/VERSION +1 -0
  7. data/bin/nestor +3 -0
  8. data/doc/.gitignore +3 -0
  9. data/doc/state-diagram.graffle +3870 -0
  10. data/doc/state-diagram.png +0 -0
  11. data/lib/nestor/cli.rb +52 -0
  12. data/lib/nestor/machine.rb +161 -0
  13. data/lib/nestor/strategies/test/unit.rb +116 -0
  14. data/lib/nestor/strategies.rb +18 -0
  15. data/lib/nestor/watchers/rails.rb +56 -0
  16. data/lib/nestor/watchers/rails_script.rb +83 -0
  17. data/lib/nestor/watchers.rb +1 -0
  18. data/lib/nestor.rb +11 -0
  19. data/spec/machine_spec.rb +56 -0
  20. data/spec/spec_helper.rb +9 -0
  21. data/vendor/watchr-0.5.7/.gitignore +5 -0
  22. data/vendor/watchr-0.5.7/History.txt +32 -0
  23. data/vendor/watchr-0.5.7/LICENSE +19 -0
  24. data/vendor/watchr-0.5.7/Manifest +27 -0
  25. data/vendor/watchr-0.5.7/README.rdoc +108 -0
  26. data/vendor/watchr-0.5.7/Rakefile +49 -0
  27. data/vendor/watchr-0.5.7/TODO.txt +40 -0
  28. data/vendor/watchr-0.5.7/bin/watchr +77 -0
  29. data/vendor/watchr-0.5.7/docs.watchr +26 -0
  30. data/vendor/watchr-0.5.7/gem.watchr +32 -0
  31. data/vendor/watchr-0.5.7/lib/watchr/controller.rb +81 -0
  32. data/vendor/watchr-0.5.7/lib/watchr/event_handlers/base.rb +48 -0
  33. data/vendor/watchr-0.5.7/lib/watchr/event_handlers/portable.rb +55 -0
  34. data/vendor/watchr-0.5.7/lib/watchr/event_handlers/unix.rb +97 -0
  35. data/vendor/watchr-0.5.7/lib/watchr/script.rb +203 -0
  36. data/vendor/watchr-0.5.7/lib/watchr.rb +113 -0
  37. data/vendor/watchr-0.5.7/manifest.watchr +70 -0
  38. data/vendor/watchr-0.5.7/specs.watchr +38 -0
  39. data/vendor/watchr-0.5.7/test/README +11 -0
  40. data/vendor/watchr-0.5.7/test/event_handlers/test_base.rb +24 -0
  41. data/vendor/watchr-0.5.7/test/event_handlers/test_portable.rb +58 -0
  42. data/vendor/watchr-0.5.7/test/event_handlers/test_unix.rb +162 -0
  43. data/vendor/watchr-0.5.7/test/test_controller.rb +103 -0
  44. data/vendor/watchr-0.5.7/test/test_helper.rb +52 -0
  45. data/vendor/watchr-0.5.7/test/test_script.rb +123 -0
  46. data/vendor/watchr-0.5.7/test/test_watchr.rb +60 -0
  47. data/vendor/watchr-0.5.7/watchr.gemspec +60 -0
  48. metadata +152 -0
@@ -0,0 +1,108 @@
1
+ === Summary
2
+
3
+ Agile development tool that monitors a directory tree, and triggers a user
4
+ defined action whenever an observed file is modified. Its most typical use is
5
+ continuous testing, and as such it is a more flexible alternative to autotest.
6
+
7
+
8
+ === Features
9
+
10
+ watchr is:
11
+
12
+ * Simple to use
13
+ * Highly flexible
14
+ * Evented ( Listens for filesystem events with native c libs )
15
+ * Portable ( Linux, *BSD, OSX, Solaris, Windows )
16
+ * Fast ( Immediately reacts to file changes )
17
+
18
+ Most importantly it allows running tests in an environment that is *agnostic* to:
19
+
20
+ * Web frameworks ( rails, merb, sinatra, camping, invisible, ... )
21
+ * Test frameworks ( test/unit, minitest, rspec, test/spec, expectations, ... )
22
+ * Ruby interpreters ( ruby1.8, ruby1.9, MRI, JRuby, Rubinius, ... )
23
+ * Package frameworks ( rubygems, rip, ... )
24
+
25
+
26
+ === Usage
27
+
28
+ On the command line,
29
+
30
+ $ watchr path/to/script.file
31
+
32
+ will monitor files in the current directory tree, and react to events on those
33
+ files in accordance with the script.
34
+
35
+
36
+ === Scripts
37
+
38
+ The script contains a set of simple rules that map observed files to an action.
39
+ Its DSL is a single method: watch(pattern, &action)
40
+
41
+ watch( 'a regexp pattern matching paths to observe' ) {|match_data_object| command_to_run }
42
+
43
+ So for example,
44
+
45
+ watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
46
+
47
+ will match any test file and run it whenever it is saved.
48
+
49
+ A continuous testing script for a basic project could be
50
+
51
+ watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
52
+ watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
53
+
54
+ which, in addition to running any saved test file as above, will also run a
55
+ lib file's associated test. This mimics the equivalent autotest behaviour.
56
+
57
+ It's easy to see why watchr is so flexible, since the whole command is custom.
58
+ The above actions could just as easily call "jruby", "ruby --rubygems", "ruby
59
+ -Ilib", "specrb", "rbx", ... or any combination of these. For the sake of
60
+ comparison, autotest runs with:
61
+
62
+ /usr/bin/ruby1.8 -I.:lib:test -rubygems -e "%w[test/unit test/test_helper.rb test/test_watchr.rb].each { |f| require f }"
63
+
64
+ locking the environment into ruby1.8, rubygems and test/unit for all tests.
65
+
66
+ And remember the scripts are pure ruby, so feel free to add methods,
67
+ Signal#trap calls, etc. Updates to script files are picked up on the fly (no
68
+ need to restart watchr) so experimenting is painless.
69
+
70
+ The wiki[http://wiki.github.com/mynyml/watchr] has more details and examples.
71
+ You might also want to take a look at watchr's own scripts,
72
+ specs.watchr[http://github.com/mynyml/watchr/blob/master/specs.watchr],
73
+ docs.watchr[http://github.com/mynyml/watchr/blob/master/docs.watchr] and
74
+ gem.watchr[http://github.com/mynyml/watchr/blob/master/gem.watchr], to get you
75
+ started.
76
+
77
+
78
+ === Install
79
+
80
+ gem install watchr --source http://gemcutter.org
81
+
82
+ If you're on *nix and have the rev[http://github.com/tarcieri/rev/] gem
83
+ installed, Watchr will detect it and use it automatically. This will make
84
+ Watchr evented.
85
+
86
+ gem install rev
87
+
88
+
89
+ === See Also
90
+
91
+ redgreen[http://github.com/mynyml/redgreen]:: Standalone redgreen eye candy for test results, ala autotest.
92
+ phocus[http://github.com/mynyml/phocus]:: Run focused tests when running the whole file/suite is unnecessary.
93
+
94
+
95
+ === Links
96
+
97
+ source:: http://github.com/mynyml/watchr
98
+ docs:: http://docs.github.com/mynyml/watchr
99
+ wiki:: http://wiki.github.com/mynyml/watchr
100
+ bugs:: http://github.com/mynyml/watchr/issues
101
+
102
+ === Contributions
103
+
104
+ macournoyer[http://github.com/macournoyer]:: suggested evented backend
105
+ foca[http://github.com/foca]:: suggested automatically picking up watchr scripts bundled in gems
106
+ TwP[http://github.com/TwP]:: patch, Rev gem optional in development
107
+ gzuki[http://github.com/gzuki]:: patch, recognize some event types
108
+
@@ -0,0 +1,49 @@
1
+ require 'rake/rdoctask'
2
+ begin
3
+ require 'yard'
4
+ rescue LoadError, RuntimeError
5
+ end
6
+
7
+ desc "Generate rdoc documentation."
8
+ Rake::RDocTask.new(:rdoc => 'rdoc', :clobber_rdoc => 'rdoc:clean', :rerdoc => 'rdoc:force') { |rdoc|
9
+ rdoc.rdoc_dir = 'doc/rdoc'
10
+ rdoc.title = "Watchr"
11
+ rdoc.options << '--line-numbers' << '--inline-source'
12
+ rdoc.options << '--charset' << 'utf-8'
13
+ rdoc.main = 'README.rdoc'
14
+ rdoc.rdoc_files.include('README.rdoc')
15
+ rdoc.rdoc_files.include('TODO.txt')
16
+ rdoc.rdoc_files.include('LICENSE')
17
+ rdoc.rdoc_files.include('lib/**/*.rb')
18
+ }
19
+
20
+ if defined? YARD
21
+ YARD::Rake::YardocTask.new do |t|
22
+ t.files = %w( lib/**/*.rb )
23
+ t.options = %w( -o doc/yard --readme README.rdoc --files LICENSE,TODO.txt )
24
+ end
25
+ end
26
+
27
+ namespace(:test) do
28
+
29
+ desc "Run all tests"
30
+ task(:all) do
31
+ tests = Dir['test/**/test_*.rb'] - ['test/test_helper.rb']
32
+ cmd = "ruby -rubygems -Ilib -e'%w( #{tests.join(' ')} ).each {|file| require file }'"
33
+ puts cmd if ENV['VERBOSE']
34
+ system cmd
35
+ end
36
+
37
+ desc "Run all tests on multiple ruby versions (requires rvm with 1.8.6 and 1.8.7)"
38
+ task(:portability) do
39
+ versions = %w( 1.8.6 1.8.7 )
40
+ versions.each do |version|
41
+ system <<-BASH
42
+ bash -c 'source ~/.rvm/scripts/rvm;
43
+ rvm use #{version};
44
+ echo "--------- `ruby -v` ----------\n";
45
+ rake -s test:all'
46
+ BASH
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+
2
+ * 1.9 compatibility
3
+
4
+ * sometimes an action is fired without a file being saved
5
+ * buffer flushing issue?
6
+ * libev issue?
7
+ * probably fixed with event type handling update, which ignores atime
8
+ updates by defaults
9
+
10
+ * when a file is saved twice quickly, subsequent events are ignored.
11
+ * seems like rev/libev drops the file watch
12
+
13
+ * test on other platforms
14
+ x mswin
15
+ x cygwin
16
+ * bsd
17
+ * osx
18
+ * solaris
19
+
20
+ * write a few prepackaged scripts
21
+ * post on gists
22
+ * post links on wiki
23
+ * post main links in readme
24
+
25
+ * eval script within own context?
26
+ * use case: using <tt>path</tt> within script accesses Script#path
27
+
28
+ * respond to different file events?
29
+ * modified
30
+ * created
31
+ * deleted
32
+ * etc.
33
+ * watch(pattern, EVENT, &action)
34
+ * use case: a script updates a manifest file when a file is deleted
35
+
36
+ * memory profiling / benchmarks
37
+
38
+ * version.watchr
39
+ * sync versions (gemspec & Watchr::VERSION)
40
+
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'optparse'
5
+
6
+ require 'watchr'
7
+
8
+ module Watchr
9
+ # Namespaced to avoid defining global methods
10
+ module Bin #:nodoc:
11
+ extend self
12
+
13
+ def usage
14
+ "Usage: watchr [opts] path/to/script"
15
+ end
16
+
17
+ def version
18
+ "watchr version: %s" % Watchr::VERSION
19
+ end
20
+
21
+ # Find a partial path name in load path
22
+ #
23
+ # ===== Params
24
+ # path<Pathname>:: partial pathname
25
+ #
26
+ # ===== Returns
27
+ # <Pathname>::
28
+ # absolute path of first occurence of partial path in load path, or nil if not found
29
+ #
30
+ def find_in_load_path(path)
31
+ dir = potentially_with_gem( path.basename('.watchr') ) do
32
+ $LOAD_PATH.detect {|p| Pathname(p).join(path).exist? }
33
+ end
34
+ dir ? path.expand_path(dir) : nil
35
+ end
36
+
37
+ private
38
+
39
+ # If the block returns nil, requires gem <tt>name</tt> and tries running the
40
+ # block again. If all fails, returns nil
41
+ #
42
+ # ===== Params
43
+ # name<Pathname,String>:: name of gem to require
44
+ #
45
+ # ===== Returns
46
+ # block's value or nil if gem <tt>name</tt> doesn't exist
47
+ #
48
+ def potentially_with_gem(name)
49
+ yield || (require(name) && yield)
50
+ rescue LoadError
51
+ nil
52
+ end
53
+ end
54
+ end
55
+
56
+ opts = OptionParser.new do |opts|
57
+ opts.banner = Watchr::Bin.usage
58
+
59
+ opts.on('-d', '--debug', "Print extra debug info while program runs") {
60
+ Watchr.options.debug = true
61
+ begin
62
+ require 'ruby-debug'
63
+ rescue LoadError, RuntimeError
64
+ end
65
+ }
66
+
67
+ opts.on_tail('-h', '--help', "Print inline help") { puts opts; exit }
68
+ opts.on_tail('-v', '--version', "Print version" ) { puts Watchr::Bin.version; exit }
69
+
70
+ opts.parse! ARGV
71
+ end
72
+
73
+ relative_path = Pathname( ARGV.first ) rescue abort(Watchr::Bin.usage)
74
+ absolute_path = Watchr::Bin.find_in_load_path(relative_path) or abort("no script found; file #{relative_path.to_s.inspect} is not in path.")
75
+
76
+ Watchr::Controller.new(Watchr::Script.new(absolute_path), Watchr.handler.new).run
77
+
@@ -0,0 +1,26 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr docs.watchr
4
+
5
+ def run_rdoc
6
+ system('rake --silent rdoc')
7
+ end
8
+
9
+ def run_yard
10
+ print "\nUpdating yardocs... "
11
+ system('rake --silent yardoc')
12
+ print "done.\n"
13
+ end
14
+
15
+ def document
16
+ run_rdoc
17
+ run_yard
18
+ end
19
+
20
+ watch( 'lib/.*\.rb' ) { document }
21
+ watch( 'README.rdoc' ) { document }
22
+ watch( 'TODO.txt' ) { document }
23
+ watch( 'LICENSE' ) { document }
24
+
25
+
26
+ # vim:ft=ruby
@@ -0,0 +1,32 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr gem.watchr
4
+
5
+ # --------------------------------------------------
6
+ # Convenience Methods
7
+ # --------------------------------------------------
8
+ def build(gemspec)
9
+ system "gem build %s" % gemspec
10
+ FileUtils.mv Dir['watchr-*.gem'], 'pkg/'
11
+ puts
12
+ end
13
+
14
+ # --------------------------------------------------
15
+ # Watchr Rules
16
+ # --------------------------------------------------
17
+ watch( '^watchr.gemspec$' ) { |m| build m[0] }
18
+
19
+ # --------------------------------------------------
20
+ # Signal Handling
21
+ # --------------------------------------------------
22
+ # Ctrl-\
23
+ Signal.trap('QUIT') do
24
+ puts " --- Building Gem ---\n\n"
25
+ build 'watchr.gemspec'
26
+ end
27
+
28
+ # Ctrl-C
29
+ Signal.trap('INT') { abort("\n") }
30
+
31
+
32
+ # vim:ft=ruby
@@ -0,0 +1,81 @@
1
+ module Watchr
2
+
3
+ # The controller contains the app's core logic.
4
+ #
5
+ # ===== Examples
6
+ #
7
+ # script = Watchr::Script.new(file)
8
+ # contrl = Watchr::Controller.new(script)
9
+ # contrl.run
10
+ #
11
+ # Calling <tt>#run</tt> will enter the listening loop, and from then on every
12
+ # file event will trigger its corresponding action defined in <tt>script</tt>
13
+ #
14
+ # The controller also automatically adds the script's file itself to its list
15
+ # of monitored files and will detect any changes to it, providing on the fly
16
+ # updates of defined rules.
17
+ #
18
+ class Controller
19
+
20
+ # Creates a controller object around given <tt>script</tt>
21
+ #
22
+ # ===== Parameters
23
+ # script<Script>:: The script object
24
+ #
25
+ def initialize(script, handler)
26
+ @script = script
27
+ @handler = handler
28
+
29
+ @handler.add_observer(self)
30
+
31
+ Watchr.debug "using %s handler" % handler.class.name
32
+ end
33
+
34
+ # Enters listening loop.
35
+ #
36
+ # Will block control flow until application is explicitly stopped/killed.
37
+ #
38
+ def run
39
+ @script.parse!
40
+ @handler.listen(monitored_paths)
41
+ end
42
+
43
+ # Callback for file events.
44
+ #
45
+ # Called while control flow in in listening loop. It will execute the
46
+ # file's corresponding action as defined in the script. If the file is the
47
+ # script itself, it will refresh its state to account for potential changes.
48
+ #
49
+ # ===== Parameters
50
+ # path<Pathname, String>:: path that triggered event
51
+ # event<Symbol>:: event type (ignored for now)
52
+ #
53
+ def update(path, event_type = nil)
54
+ path = Pathname(path).expand_path
55
+
56
+ if path == @script.path
57
+ @script.parse!
58
+ @handler.refresh(monitored_paths)
59
+ else
60
+ @script.action_for(path, event_type).call
61
+ end
62
+ end
63
+
64
+ # List of paths the script is monitoring.
65
+ #
66
+ # Basically this means all paths below current directoly recursivelly that
67
+ # match any of the rules' patterns, plus the script file.
68
+ #
69
+ # ===== Returns
70
+ # paths<Array[Pathname]>:: List of monitored paths
71
+ #
72
+ def monitored_paths
73
+ paths = Dir['**/*'].select do |path|
74
+ @script.patterns.any? {|p| path.match(p) }
75
+ end
76
+ paths.push(@script.path).compact!
77
+ paths.map {|path| Pathname(path).expand_path }
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,48 @@
1
+ require 'observer'
2
+
3
+ module Watchr
4
+ module EventHandler
5
+ class AbstractMethod < Exception #:nodoc:
6
+ end
7
+
8
+ # Base functionality mixin meant to be included in specific event handlers.
9
+ module Base
10
+ include Observable
11
+
12
+ # Notify that a file was modified.
13
+ #
14
+ # ===== Parameters
15
+ # path<Pathname, String>:: full path or path relative to current working directory
16
+ # event_type<Symbol>:: event type.
17
+ #--
18
+ # #changed and #notify_observers are Observable methods
19
+ def notify(path, event_type = nil)
20
+ changed(true)
21
+ notify_observers(path, event_type)
22
+ end
23
+
24
+ # Begin watching given paths and enter listening loop. Called by the controller.
25
+ #
26
+ # Abstract method
27
+ #
28
+ # ===== Parameters
29
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
30
+ #
31
+ def listen(monitored_paths)
32
+ raise AbstractMethod
33
+ end
34
+
35
+ # Called by the controller when the list of paths monitored by wantchr
36
+ # has changed. It should refresh the list of paths being watched.
37
+ #
38
+ # Abstract method
39
+ #
40
+ # ===== Parameters
41
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
42
+ #
43
+ def refresh(monitored_paths)
44
+ raise AbstractMethod
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ module Watchr
2
+ module EventHandler
3
+ class Portable
4
+ include Base
5
+
6
+ attr_accessor :monitored_paths
7
+ attr_accessor :reference_mtime
8
+
9
+ def initialize
10
+ @reference_mtime = Time.now
11
+ end
12
+
13
+ # Enters listening loop.
14
+ #
15
+ # Will block control flow until application is explicitly stopped/killed.
16
+ #
17
+ def listen(monitored_paths)
18
+ @monitored_paths = monitored_paths
19
+ loop { trigger; sleep(1) }
20
+ end
21
+
22
+ # See if an event occured, and if so notify observers.
23
+ def trigger #:nodoc:
24
+ path, type = detect_event
25
+ notify(path, type) unless path.nil?
26
+ end
27
+
28
+ # Update list of monitored paths.
29
+ def refresh(monitored_paths)
30
+ @monitored_paths = monitored_paths
31
+ end
32
+
33
+ private
34
+
35
+ # Verify mtimes of monitored files.
36
+ #
37
+ # If the latest mtime is more recent than the reference mtime, return
38
+ # that file's path.
39
+ #
40
+ # ===== Returns
41
+ # path and type of event if event occured, nil otherwise
42
+ #
43
+ def detect_event
44
+ path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime }
45
+
46
+ if path.mtime > @reference_mtime
47
+ @reference_mtime = path.mtime
48
+ [path, :modified]
49
+ else
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,97 @@
1
+ module Watchr
2
+ module EventHandler
3
+ class Unix
4
+ include Base
5
+
6
+ # Used by Rev. Wraps a monitored path, and Rev::Loop will call its
7
+ # callback on file events.
8
+ class SingleFileWatcher < Rev::StatWatcher #:nodoc:
9
+ class << self
10
+ # Stores a reference back to handler so we can call its #nofity
11
+ # method with file event info
12
+ attr_accessor :handler
13
+ end
14
+
15
+ def initialize(path)
16
+ super
17
+ update_reference_times
18
+ end
19
+
20
+ # File's path as a Pathname
21
+ def pathname
22
+ @pathname ||= Pathname(@path)
23
+ end
24
+
25
+ # Callback. Called on file change event
26
+ # Delegates to Controller#update, passing in path and event type
27
+ def on_change
28
+ self.class.handler.notify(path, type)
29
+ update_reference_times unless type == :deleted
30
+ end
31
+
32
+ private
33
+
34
+ def update_reference_times
35
+ @reference_atime = pathname.atime
36
+ @reference_mtime = pathname.mtime
37
+ @reference_ctime = pathname.ctime
38
+ end
39
+
40
+ # Type of latest event.
41
+ #
42
+ # A single type is determined, even though more than one stat times may
43
+ # have changed on the file. The type is the first to match in the
44
+ # following hierarchy:
45
+ #
46
+ # :deleted, :modified (mtime), :accessed (atime), :changed (ctime)
47
+ #
48
+ # ===== Returns
49
+ # type<Symbol>:: latest event's type
50
+ #
51
+ def type
52
+ return :deleted if !pathname.exist?
53
+ return :modified if pathname.mtime > @reference_mtime
54
+ return :accessed if pathname.atime > @reference_atime
55
+ return :changed if pathname.ctime > @reference_ctime
56
+ end
57
+ end
58
+
59
+ def initialize
60
+ SingleFileWatcher.handler = self
61
+ @loop = Rev::Loop.default
62
+ end
63
+
64
+ # Enters listening loop.
65
+ #
66
+ # Will block control flow until application is explicitly stopped/killed.
67
+ #
68
+ def listen(monitored_paths)
69
+ @monitored_paths = monitored_paths
70
+ attach
71
+ @loop.run
72
+ end
73
+
74
+ # Rebuilds file bindings.
75
+ #
76
+ # will detach all current bindings, and reattach the <tt>monitored_paths</tt>
77
+ #
78
+ def refresh(monitored_paths)
79
+ @monitored_paths = monitored_paths
80
+ detach
81
+ attach
82
+ end
83
+
84
+ private
85
+
86
+ # Binds all <tt>monitored_paths</tt> to the listening loop.
87
+ def attach
88
+ @monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) }
89
+ end
90
+
91
+ # Unbinds all paths currently attached to listening loop.
92
+ def detach
93
+ @loop.watchers.each {|watcher| watcher.detach }
94
+ end
95
+ end
96
+ end
97
+ end