akqa-jenkins 0.6.4.1

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 (106) hide show
  1. data/.gitignore +8 -0
  2. data/Changelog.md +54 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +56 -0
  5. data/README.md +159 -0
  6. data/Rakefile +82 -0
  7. data/bin/jenkins +11 -0
  8. data/cucumber.yml +8 -0
  9. data/features/build_details.feature +42 -0
  10. data/features/configure.feature +36 -0
  11. data/features/default_host.feature +26 -0
  12. data/features/development.feature +14 -0
  13. data/features/launch_server.feature +16 -0
  14. data/features/listing_jobs.feature +34 -0
  15. data/features/manage_jobs.feature +214 -0
  16. data/features/manage_slave_nodes.feature +82 -0
  17. data/features/step_definitions/common_steps.rb +192 -0
  18. data/features/step_definitions/fixture_project_steps.rb +8 -0
  19. data/features/step_definitions/jenkins_steps.rb +116 -0
  20. data/features/step_definitions/scm_steps.rb +12 -0
  21. data/features/support/common.rb +37 -0
  22. data/features/support/env.rb +19 -0
  23. data/features/support/hooks.rb +16 -0
  24. data/features/support/jenkins_helpers.rb +6 -0
  25. data/features/support/matchers.rb +10 -0
  26. data/fixtures/jenkins/envfile.hpi +0 -0
  27. data/fixtures/jenkins/git.hpi +0 -0
  28. data/fixtures/jenkins/github.hpi +0 -0
  29. data/fixtures/jenkins/greenballs.hpi +0 -0
  30. data/fixtures/jenkins/rake.hpi +0 -0
  31. data/fixtures/jenkins/ruby.hpi +0 -0
  32. data/fixtures/projects/erlang/rebar.config +1 -0
  33. data/fixtures/projects/non-bundler/Rakefile +4 -0
  34. data/fixtures/projects/rails-3/.gitignore +4 -0
  35. data/fixtures/projects/rails-3/Gemfile +30 -0
  36. data/fixtures/projects/rails-3/Gemfile.lock +74 -0
  37. data/fixtures/projects/rails-3/README +256 -0
  38. data/fixtures/projects/rails-3/Rakefile +7 -0
  39. data/fixtures/projects/rails-3/app/controllers/application_controller.rb +3 -0
  40. data/fixtures/projects/rails-3/app/helpers/application_helper.rb +2 -0
  41. data/fixtures/projects/rails-3/app/views/layouts/application.html.erb +14 -0
  42. data/fixtures/projects/rails-3/config.ru +4 -0
  43. data/fixtures/projects/rails-3/config/application.rb +42 -0
  44. data/fixtures/projects/rails-3/config/boot.rb +13 -0
  45. data/fixtures/projects/rails-3/config/database.yml +22 -0
  46. data/fixtures/projects/rails-3/config/environment.rb +5 -0
  47. data/fixtures/projects/rails-3/config/environments/development.rb +26 -0
  48. data/fixtures/projects/rails-3/config/environments/production.rb +49 -0
  49. data/fixtures/projects/rails-3/config/environments/test.rb +35 -0
  50. data/fixtures/projects/rails-3/config/initializers/backtrace_silencers.rb +7 -0
  51. data/fixtures/projects/rails-3/config/initializers/inflections.rb +10 -0
  52. data/fixtures/projects/rails-3/config/initializers/mime_types.rb +5 -0
  53. data/fixtures/projects/rails-3/config/initializers/secret_token.rb +7 -0
  54. data/fixtures/projects/rails-3/config/initializers/session_store.rb +8 -0
  55. data/fixtures/projects/rails-3/config/locales/en.yml +5 -0
  56. data/fixtures/projects/rails-3/config/routes.rb +58 -0
  57. data/fixtures/projects/rails-3/db/seeds.rb +7 -0
  58. data/fixtures/projects/rails-3/doc/README_FOR_APP +2 -0
  59. data/fixtures/projects/rails-3/lib/tasks/.gitkeep +0 -0
  60. data/fixtures/projects/rails-3/public/404.html +26 -0
  61. data/fixtures/projects/rails-3/public/422.html +26 -0
  62. data/fixtures/projects/rails-3/public/500.html +26 -0
  63. data/fixtures/projects/rails-3/public/favicon.ico +0 -0
  64. data/fixtures/projects/rails-3/public/images/rails.png +0 -0
  65. data/fixtures/projects/rails-3/public/index.html +239 -0
  66. data/fixtures/projects/rails-3/public/javascripts/application.js +2 -0
  67. data/fixtures/projects/rails-3/public/javascripts/controls.js +965 -0
  68. data/fixtures/projects/rails-3/public/javascripts/dragdrop.js +974 -0
  69. data/fixtures/projects/rails-3/public/javascripts/effects.js +1123 -0
  70. data/fixtures/projects/rails-3/public/javascripts/prototype.js +6001 -0
  71. data/fixtures/projects/rails-3/public/javascripts/rails.js +175 -0
  72. data/fixtures/projects/rails-3/public/robots.txt +5 -0
  73. data/fixtures/projects/rails-3/public/stylesheets/.gitkeep +0 -0
  74. data/fixtures/projects/rails-3/script/rails +6 -0
  75. data/fixtures/projects/rails-3/test/performance/browsing_test.rb +9 -0
  76. data/fixtures/projects/rails-3/test/test_helper.rb +13 -0
  77. data/fixtures/projects/rails-3/vendor/plugins/.gitkeep +0 -0
  78. data/fixtures/projects/ruby/Gemfile +3 -0
  79. data/fixtures/projects/ruby/Gemfile.lock +10 -0
  80. data/fixtures/projects/ruby/Rakefile +4 -0
  81. data/jenkins.gemspec +33 -0
  82. data/lib/jenkins.rb +6 -0
  83. data/lib/jenkins/api.rb +250 -0
  84. data/lib/jenkins/cli.rb +291 -0
  85. data/lib/jenkins/cli/formatting.rb +53 -0
  86. data/lib/jenkins/config.rb +27 -0
  87. data/lib/jenkins/core_ext/hash.rb +9 -0
  88. data/lib/jenkins/core_ext/object/blank.rb +77 -0
  89. data/lib/jenkins/hudson-cli.jar +0 -0
  90. data/lib/jenkins/job_config_builder.rb +292 -0
  91. data/lib/jenkins/project_scm.rb +22 -0
  92. data/lib/jenkins/remote.rb +11 -0
  93. data/lib/jenkins/version.rb +3 -0
  94. data/spec/fixtures/ec2_global.config.xml +103 -0
  95. data/spec/fixtures/erlang.single.config.xml +59 -0
  96. data/spec/fixtures/rails.multi.config.xml +82 -0
  97. data/spec/fixtures/rails.single.config.triggers.xml +84 -0
  98. data/spec/fixtures/rails.single.config.xml +80 -0
  99. data/spec/fixtures/ruby.multi-ruby-multi-labels.config.xml +84 -0
  100. data/spec/fixtures/ruby.multi.config.xml +77 -0
  101. data/spec/fixtures/ruby.single.config.xml +58 -0
  102. data/spec/fixtures/therubyracer.config.xml +77 -0
  103. data/spec/hash_key_cleaner_spec.rb +25 -0
  104. data/spec/job_config_builder_spec.rb +150 -0
  105. data/spec/spec_helper.rb +15 -0
  106. metadata +283 -0
@@ -0,0 +1,291 @@
1
+ require 'thor'
2
+ require 'jenkins/core_ext/object/blank'
3
+ require 'jenkins/core_ext/hash'
4
+ require 'jenkins/cli/formatting'
5
+ require 'jenkins/remote'
6
+
7
+ module Jenkins
8
+ class CLI < Thor
9
+ include CLI::Formatting
10
+
11
+ map "-v" => :version, "--version" => :version, "-h" => :help, "--help" => :help
12
+
13
+ def self.common_options
14
+ method_option :host, :desc => 'connect to jenkins server on this host'
15
+ method_option :port, :desc => 'connect to jenkins server on this port'
16
+ method_option :ssl, :desc => 'connect to jenkins server with ssl', :type => :boolean, :default => false
17
+ method_option :username, :desc => 'connect to jenkins server with username'
18
+ method_option :password, :desc => 'connect to jenkins server with password'
19
+ end
20
+
21
+ desc "server [options]", "run a jenkins server"
22
+ method_option :home, :desc => "use this directory to store server data", :type => :string, :default => File.join(ENV['HOME'], ".jenkins", "server"), :banner => "PATH"
23
+ method_option :port, :desc => "run jenkins server on this port", :type => :numeric, :default => 3001, :aliases => "-p"
24
+ method_option :control, :desc => "set the shutdown/control port", :type => :numeric, :default => 3002, :aliases => "-c"
25
+ method_option :daemon, :desc => "fork into background and run as a daemon", :type => :boolean, :default => false
26
+ method_option :kill, :desc => "send shutdown signal to control port", :type => :boolean, :aliases => "-k"
27
+ method_option :logfile, :desc => "redirect log messages to this file", :type => :string, :banner => "PATH"
28
+ def server
29
+ begin
30
+ require 'jenkins/war'
31
+ Jenkins::War::server(options)
32
+ rescue LoadError
33
+ puts "To run a jenkins server, you need to install the jenkins-war gem. try:"
34
+ puts "gem install jenkins-war"
35
+ end
36
+ end
37
+
38
+ desc "create project_path [options]", "create a build for your project"
39
+ common_options
40
+ method_option :rubies, :desc => "run tests against multiple explicit rubies via RVM", :type => :string
41
+ method_option :"node-labels", :desc => "run tests against multiple slave nodes by their label (comma separated)"
42
+ method_option :"assigned-node", :desc => "only use slave nodes with this label (similar to --node-labels)"
43
+ method_option :"no-build", :desc => "create job without initial build", :type => :boolean, :default => false
44
+ method_option :override, :desc => "override if job exists", :type => :boolean, :default => false
45
+ method_option :"scm", :desc => "specific SCM URI", :type => :string
46
+ method_option :"scm-branches", :desc => "list of branches to build from (comma separated)", :type => :string, :default => "master"
47
+ method_option :"public-scm", :desc => "use public scm URL", :type => :boolean, :default => false
48
+ method_option :template, :desc => "template of job steps (available: #{JobConfigBuilder::VALID_JOB_TEMPLATES.join ','})", :default => 'ruby'
49
+ method_option :"no-template", :desc => "do not use a template of default steps; avoids Gemfile requirement", :type => :boolean, :default => false
50
+ def create(project_path)
51
+ select_jenkins_server(options)
52
+ FileUtils.chdir(project_path) do
53
+ unless scm = Jenkins::ProjectScm.discover(options[:scm])
54
+ error "Cannot determine project SCM. Currently supported: #{Jenkins::ProjectScm.supported}"
55
+ end
56
+ unless (options[:template] == "none" || options[:template] == "erlang" || options[:"no-template"]) || File.exists?("Gemfile")
57
+ error "Ruby/Rails projects without a Gemfile are currently unsupported."
58
+ end
59
+ begin
60
+ template = options[:"no-template"] ? 'none' : options[:template]
61
+ job_config = Jenkins::JobConfigBuilder.new(template) do |c|
62
+ c.rubies = options[:rubies].split(/\s*,\s*/) if options[:rubies]
63
+ c.node_labels = options[:"node-labels"].split(/\s*,\s*/) if options[:"node-labels"]
64
+ c.scm = scm.url
65
+ c.scm_branches = options[:"scm-branches"].split(/\s*,\s*/)
66
+ c.assigned_node = options[:"assigned-node"] if options[:"assigned-node"]
67
+ c.public_scm = options[:"public-scm"]
68
+ end
69
+ name = File.basename(FileUtils.pwd)
70
+ if Jenkins::Api.create_job(name, job_config, options)
71
+ build_url = "#{@uri}/job/#{name.gsub(/\s/,'%20')}/build"
72
+ shell.say "Added#{' ' + template unless template == 'none'} project '#{name}' to Jenkins.", :green
73
+ unless options[:"no-build"]
74
+ shell.say "Triggering initial build..."
75
+ Jenkins::Api.build_job(name)
76
+ shell.say "Trigger additional builds via:"
77
+ else
78
+ shell.say "Trigger builds via:"
79
+ end
80
+ shell.say " URL: "; shell.say "#{build_url}", :yellow
81
+ shell.say " CLI: "; shell.say "#{cmd} build #{name}", :yellow
82
+ else
83
+ error "Failed to create project '#{name}'"
84
+ end
85
+ rescue Jenkins::JobConfigBuilder::InvalidTemplate
86
+ error "Invalid job template '#{template}'."
87
+ rescue Jenkins::Api::JobAlreadyExistsError
88
+ error "Job '#{name}' already exists."
89
+ end
90
+ end
91
+ end
92
+
93
+ desc "build [PROJECT_PATH]", "trigger build of this project's build job"
94
+ common_options
95
+ def build(project_path = ".")
96
+ select_jenkins_server(options)
97
+ FileUtils.chdir(project_path) do
98
+ name = File.basename(FileUtils.pwd)
99
+ if Jenkins::Api.build_job(name)
100
+ shell.say "Build for '#{name}' running now..."
101
+ else
102
+ error "No job '#{name}' on server."
103
+ end
104
+ end
105
+ end
106
+
107
+ desc "remove PROJECT_PATH", "remove this project's build job from Jenkins"
108
+ common_options
109
+ def remove(project_path)
110
+ select_jenkins_server(options)
111
+ FileUtils.chdir(project_path) do
112
+ name = File.basename(FileUtils.pwd)
113
+ if Jenkins::Api.delete_job(name)
114
+ shell.say "Removed project '#{name}' from Jenkins."
115
+ else
116
+ error "Failed to delete project '#{name}'."
117
+ end
118
+ end
119
+ end
120
+
121
+ desc "job NAME", "Display job details"
122
+ method_option :hash, :desc => 'Dump as formatted Ruby hash format'
123
+ method_option :json, :desc => 'Dump as JSON format'
124
+ method_option :yaml, :desc => 'Dump as YAML format'
125
+ common_options
126
+ def job(name)
127
+ select_jenkins_server(options)
128
+ if job = Jenkins::Api.job(name)
129
+ if options[:hash]
130
+ require "ap"
131
+ ap job.parsed_response
132
+ elsif options[:json]
133
+ puts job.parsed_response.to_json
134
+ elsif options[:yaml]
135
+ require "yaml"
136
+ puts job.parsed_response.to_yaml
137
+ else
138
+ error "Select an output format: --json, --xml, --yaml, --hash"
139
+ end
140
+ else
141
+ error "Cannot find project '#{name}'."
142
+ end
143
+ end
144
+
145
+ desc "build_details JOB_NAME BUILD_NUMBER", "Display details about a particular build"
146
+ method_option :hash, :desc => 'Dump as formatted Ruby hash format'
147
+ method_option :json, :desc => 'Dump as JSON format'
148
+ method_option :yaml, :desc => 'Dump as YAML format'
149
+ common_options
150
+ def build_details(job_name, build_number)
151
+ select_jenkins_server(options)
152
+ if build_details = Jenkins::Api.build_details(job_name, build_number)
153
+ if options[:hash]
154
+ require "ap"
155
+ ap build_details.parsed_response
156
+ elsif options[:json]
157
+ puts build_details.parsed_response.to_json
158
+ elsif options[:yaml]
159
+ require "yaml"
160
+ puts build_details.parsed_response.to_yaml
161
+ else
162
+ error "Select an output format: --json, --yaml, --hash"
163
+ end
164
+ else
165
+ error "Cannot find project '#{job_name}'."
166
+ end
167
+ end
168
+
169
+ desc "list [options]", "list jobs on a jenkins server"
170
+ common_options
171
+ def list
172
+ select_jenkins_server(options)
173
+ summary = Jenkins::Api.summary
174
+ unless summary["jobs"].blank?
175
+ shell.say "#{@uri}:", :bold
176
+ summary["jobs"].each do |job|
177
+ bold = job['color'] =~ /anime/
178
+ color = 'red' if job['color'] =~ /red/
179
+ color = 'green' if job['color'] =~ /(blue|green)/
180
+ color ||= 'yellow' # if color =~ /grey/ || color == 'disabled'
181
+ shell.say "* "; shell.say(shell.set_color(job['name'], color.to_sym, bold), nil, true)
182
+ end
183
+ shell.say ""
184
+ else
185
+ shell.say "#{@uri}: "; shell.say "no jobs", :yellow
186
+ end
187
+ end
188
+
189
+ desc "nodes", "list jenkins server nodes"
190
+ common_options
191
+ def nodes
192
+ select_jenkins_server(options)
193
+ nodes = Jenkins::Api.nodes
194
+ nodes["computer"].each do |node|
195
+ color = node["offline"] ? :red : :green
196
+ shell.say node["displayName"], color
197
+ end
198
+ end
199
+
200
+ desc "add_node SLAVE_HOST", "add a URI (user@host:port) server as a slave node"
201
+ method_option :labels, :desc => 'Labels for a job --assigned_node to match against to select a slave (comma separated)'
202
+ method_option :"slave-user", :desc => 'SSH user for Jenkins to connect to slave node (default: deploy)'
203
+ method_option :"slave-port", :desc => 'SSH port for Jenkins to connect to slave node (default: 22)'
204
+ method_option :"master-key", :desc => 'Location of master public key or identity file'
205
+ method_option :"slave-fs", :desc => 'Location of file system on slave for Jenkins to use'
206
+ method_option :name, :desc => 'Name of slave node (default SLAVE_HOST)'
207
+ method_option :vagrant, :desc => 'Use settings for a Vagrant VM', :type => :boolean, :default => false
208
+ common_options
209
+ def add_node(slave_host)
210
+ select_jenkins_server(options)
211
+ if results = Jenkins::Api.add_node({:slave_host => slave_host}.merge(options))
212
+ shell.say "Added slave node '#{results[:name]}' to #{results[:slave_host]}", :green
213
+ else
214
+ error "Failed to add slave node #{slave_host}"
215
+ end
216
+ end
217
+
218
+ desc "default_host", "display current default host:port URI"
219
+ def default_host
220
+ if select_jenkins_server({})
221
+ display Jenkins::Api.base_uri
222
+ else
223
+ display "No default host yet. Use '--host host --port port' on your first request."
224
+ end
225
+ end
226
+
227
+ desc "configure", "configure host connection information"
228
+ common_options
229
+ def configure
230
+ Jenkins::Api.setup_base_url(options)
231
+ Jenkins::Api.cache_configuration!
232
+ end
233
+
234
+ desc "auth_info", "display authentication information"
235
+ def auth_info
236
+ if auth = Jenkins::Config.config['basic_auth']
237
+ shell.say "username: #{auth["username"]}"
238
+ shell.say "password: #{auth["password"]}"
239
+ end
240
+ end
241
+
242
+ desc "help [command]", "show help for jenkins or for a specific command"
243
+ def help(*args)
244
+ super(*args)
245
+ end
246
+
247
+ desc "version", "show version information"
248
+ def version
249
+ shell.say "#{Jenkins::VERSION}"
250
+ end
251
+
252
+ def self.help(shell, *)
253
+ list = printable_tasks
254
+ shell.say <<-USEAGE
255
+ Jenkins.rb is a smart set of utilities for making
256
+ continuous integration as simple as possible
257
+
258
+ Usage: jenkins command [arguments] [options]
259
+
260
+ USEAGE
261
+
262
+ shell.say "Commands:"
263
+ shell.print_table(list, :ident => 2, :truncate => true)
264
+ shell.say
265
+ class_options_help(shell)
266
+ end
267
+
268
+ private
269
+
270
+ def select_jenkins_server(options)
271
+ unless @uri = Jenkins::Api.setup_base_url(options)
272
+ error "Either use --host or add remote servers."
273
+ end
274
+ @uri
275
+ end
276
+
277
+ def display(text)
278
+ shell.say text
279
+ exit
280
+ end
281
+
282
+ def error(text)
283
+ shell.say "ERROR: #{text}", :red
284
+ exit
285
+ end
286
+
287
+ def cmd
288
+ ENV['CUCUMBER_RUNNING'] ? 'jenkins' : $0
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,53 @@
1
+ require 'term/ansicolor'
2
+
3
+ module Jenkins
4
+ class CLI < Thor
5
+ module Formatting
6
+ module ClassMethods
7
+ def task_help(shell, task_name)
8
+ meth = normalize_task_name(task_name)
9
+ task = all_tasks[meth]
10
+ handle_no_task_error(meth) unless task
11
+
12
+ shell.say "usage: #{banner(task)}"
13
+ shell.say
14
+ class_options_help(shell, nil => task.options.map { |_, o| o })
15
+ # shell.say task.description
16
+ # shell.say
17
+ end
18
+
19
+
20
+ def print_options(shell, options, grp = nil)
21
+ return if options.empty?
22
+ table = options.map do |option|
23
+ prototype = if option.default
24
+ " [#{option.default}]"
25
+ elsif option.boolean?
26
+ ""
27
+ elsif option.required?
28
+ " #{option.banner}"
29
+ else
30
+ " [#{option.banner}]"
31
+ end
32
+ aliases = option.aliases.empty? ? "" : option.aliases.join(" ") + ","
33
+ [aliases, "--#{option.name}#{prototype}", "\t",option.description]
34
+ end
35
+ shell.print_table(table, :ident => 2)
36
+ shell.say
37
+ end
38
+ end
39
+
40
+ module InstanceMethods
41
+ def c
42
+ Term::ANSIColor
43
+ end
44
+ end
45
+
46
+ def self.included(receiver)
47
+ receiver.extend ClassMethods
48
+ receiver.send :include, InstanceMethods
49
+ end
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,27 @@
1
+ module Jenkins
2
+ module Config
3
+ extend self
4
+
5
+ def [](key)
6
+ config[key]
7
+ end
8
+
9
+ def config
10
+ @config ||= if File.exist?(config_file)
11
+ JSON.parse(File.read(config_file))
12
+ else
13
+ {}
14
+ end
15
+ end
16
+
17
+ def store!
18
+ @config ||= {}
19
+ FileUtils.mkdir_p(File.dirname(config_file))
20
+ File.open(config_file, "w") { |file| file << @config.to_json }
21
+ end
22
+
23
+ def config_file
24
+ @config_file ||= "#{ENV['HOME']}/.jenkins/jenkinsrb-config.json"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def with_clean_keys
3
+ self.inject({}) do |mem, (key, value)|
4
+ clean_key = key.to_s.gsub(/-/,"_").to_sym
5
+ mem[clean_key] = value
6
+ mem
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,77 @@
1
+ # Vendored from activesupport 3.0.1
2
+ class Object
3
+ # An object is blank if it's false, empty, or a whitespace string.
4
+ # For example, "", " ", +nil+, [], and {} are blank.
5
+ #
6
+ # This simplifies:
7
+ #
8
+ # if !address.nil? && !address.empty?
9
+ #
10
+ # ...to:
11
+ #
12
+ # if !address.blank?
13
+ def blank?
14
+ respond_to?(:empty?) ? empty? : !self
15
+ end
16
+
17
+ # An object is present if it's not blank.
18
+ def present?
19
+ !blank?
20
+ end
21
+
22
+ # Returns object if it's #present? otherwise returns nil.
23
+ # object.presence is equivalent to object.present? ? object : nil.
24
+ #
25
+ # This is handy for any representation of objects where blank is the same
26
+ # as not present at all. For example, this simplifies a common check for
27
+ # HTTP POST/query parameters:
28
+ #
29
+ # state = params[:state] if params[:state].present?
30
+ # country = params[:country] if params[:country].present?
31
+ # region = state || country || 'US'
32
+ #
33
+ # ...becomes:
34
+ #
35
+ # region = params[:state].presence || params[:country].presence || 'US'
36
+ def presence
37
+ self if present?
38
+ end
39
+ end
40
+
41
+ class NilClass #:nodoc:
42
+ def blank?
43
+ true
44
+ end
45
+ end
46
+
47
+ class FalseClass #:nodoc:
48
+ def blank?
49
+ true
50
+ end
51
+ end
52
+
53
+ class TrueClass #:nodoc:
54
+ def blank?
55
+ false
56
+ end
57
+ end
58
+
59
+ class Array #:nodoc:
60
+ alias_method :blank?, :empty?
61
+ end
62
+
63
+ class Hash #:nodoc:
64
+ alias_method :blank?, :empty?
65
+ end
66
+
67
+ class String #:nodoc:
68
+ def blank?
69
+ self !~ /\S/
70
+ end
71
+ end
72
+
73
+ class Numeric #:nodoc:
74
+ def blank?
75
+ false
76
+ end
77
+ end