Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
---|---|---|---|---|
lib/bee_util.rb | 421 | 286 | 94.54%
|
91.96%
|
Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.
1 # Copyright 2006-2012 Michel Casabianca <michel.casabianca@gmail.com> |
2 # 2006 Avi Bryant |
3 # |
4 # Licensed under the Apache License, Version 2.0 (the "License"); |
5 # you may not use this file except in compliance with the License. |
6 # You may obtain a copy of the License at |
7 # |
8 # http://www.apache.org/licenses/LICENSE-2.0 |
9 # |
10 # Unless required by applicable law or agreed to in writing, software |
11 # distributed under the License is distributed on an "AS IS" BASIS, |
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 # See the License for the specific language governing permissions and |
14 # limitations under the License. |
15 |
16 require 'rubygems' |
17 require 'net/http' |
18 require 'bee_version_dependant' |
19 |
20 module Bee |
21 |
22 module Util |
23 |
24 # Limit of number of HTTP redirections to follow. |
25 HTTP_REDIRECTIONS_LIMIT = 10 |
26 # Default package name. |
27 DEFAULT_PACKAGE = 'default' |
28 # Compact pattern for resource (':gem.file[version]') |
29 COMPACT_PATTERN = /^:(.*?):(.*?)(\[(.*)\])?$/ |
30 # Expanded pattern for resource ('ruby://gem:version/file') |
31 EXPANDED_PATTERN = /^ruby:\/\/(.*?)(:(.*?))?\/(.*)$/ |
32 # Default terminal width |
33 DEFAULT_TERM_WIDTH = (RUBY_PLATFORM =~ /win32/ ? 79 : 80) |
34 |
35 # Get line length calling IOCTL. Return DEFAULT_TERM_WIDTH if call failed. |
36 def self.term_width |
37 begin |
38 tiocgwinsz = RUBY_PLATFORM =~ /darwin/ ? 0x40087468 : 0x5413 |
39 string = [0, 0, 0, 0].pack('SSSS') |
40 if $stdin.ioctl(tiocgwinsz, string) >= 0 then |
41 rows, cols, xpixels, ypixels = string.unpack('SSSS') |
42 cols = DEFAULT_TERM_WIDTH if cols <= 0 |
43 return cols |
44 else |
45 return DEFAULT_TERM_WIDTH |
46 end |
47 rescue |
48 return DEFAULT_TERM_WIDTH |
49 end |
50 end |
51 |
52 # Tells if we are running under Windows. |
53 def self.windows? |
54 return RUBY_PLATFORM =~ /(mswin|ming)/ |
55 end |
56 |
57 # Parse packaged name and return package and name. |
58 # - packaged: packaged name (such as 'foo.bar'). |
59 # Return: package ('foo') and name ('bar'). |
60 def self.get_package_name(packaged) |
61 if packaged =~ /\./ |
62 package, name = packaged.split('.') |
63 else |
64 package, name = DEFAULT_PACKAGE, packaged |
65 end |
66 return package, name |
67 end |
68 |
69 # Get a given file or URL. Manages HTTP redirections. |
70 # - location: file path, resource or URL of the file to get. |
71 # - base: base for relative files (defaults to nil, which is current dir). |
72 def self.get_file(location, base=nil) |
73 base = base || Dir.pwd |
74 abs = absolute_path(location, base) |
75 if abs =~ /^http:/ |
76 # this is HTTP |
77 return fetch(abs) |
78 else |
79 # this is a file |
80 return File.read(abs) |
81 end |
82 end |
83 |
84 private |
85 |
86 # Looks recursively up in file system for a file. |
87 # - file: file name to look for. |
88 # Return: found file or raises an exception if file was not found. |
89 def self.find(file) |
90 return file if File.exists?(file) |
91 raise "File not found" if File.identical?(File.dirname(file), '/') |
92 file = File.join('..', file) |
93 find(file) |
94 end |
95 |
96 # Tells is a given location is a URL (starting with 'http://'). |
97 # - location: location to consider as a string. |
98 def self.url?(location) |
99 return false if not location.kind_of?(String) |
100 return location =~ /^http:\/\// |
101 end |
102 |
103 # Tells is a given location is a resource (starting with 'ruby://' or ':'). |
104 # - location: location to consider as a string. |
105 def self.resource?(location) |
106 return false if not location.kind_of?(String) |
107 return location =~ /^ruby:\/\// || location =~ /^:/ |
108 end |
109 |
110 # Tells if a given path is absolute. |
111 # - path: path to consider. |
112 def self.absolute_path?(path) |
113 if url?(path) or resource?(path) |
114 return true |
115 else |
116 if windows? |
117 return path =~ /^(([a-zA-Z]):)?\// |
118 else |
119 return path =~ /^\// |
120 end |
121 end |
122 end |
123 |
124 # Return absolute path for a given path and optional base: |
125 # - path: relative path to get absolute path for. |
126 # - base: optional base for path (defaults to current directory). |
127 def self.absolute_path(path, base=nil) |
128 path = File.expand_path(path) if path =~ /~.*/ |
129 if absolute_path?(path) |
130 if resource?(path) |
131 path = resource_path(path) |
132 end |
133 return path |
134 else |
135 base = Dir.pwd if not base |
136 return File.join(base, path) |
137 end |
138 end |
139 |
140 # Get a given URL. |
141 # - url: URL to get. |
142 # - limit: redirectrion limit (defaults to HTTP_REDIRECTIONS_LIMIT). |
143 def self.fetch(url, limit=HTTP_REDIRECTIONS_LIMIT, |
144 username=nil, password=nil) |
145 raise 'HTTP redirect too deep' if limit == 0 |
146 response = Net::HTTP.get_response(URI.parse(url)) |
147 case response |
148 when Net::HTTPSuccess |
149 response.body |
150 when Net::HTTPRedirection |
151 fetch(response['location'], limit-1) |
152 when Net::HTTPUnauthorized |
153 uri = URI.parse(url) |
154 Net::HTTP.start(uri.host, uri.port) do |http| |
155 request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}") |
156 request.basic_auth(username, password) |
157 response = http.request(request) |
158 return response.body |
159 end |
160 else |
161 response.error! |
162 end |
163 end |
164 |
165 # Return absolute path to a given resoure: |
166 # - resource: the resource (expanded patterns are like ':gem.file[version]' |
167 # and compact ones like 'ruby://gem:version/file'). |
168 def self.resource_path(resource) |
169 # get gem, version and path from resource or interrupt build with an error |
170 case resource |
171 when COMPACT_PATTERN |
172 gem, version, path = $1, $4, $2 |
173 gem = "bee_#{gem}" if gem != 'bee' |
174 when EXPANDED_PATTERN |
175 gem, version, path = $1, $3, $4 |
176 else |
177 raise "'#{resource}' is not a valid resource" |
178 end |
179 # get gem descriptor |
180 if version |
181 if Gem::Specification.respond_to?(:find_by_name) |
182 begin |
183 gem_descriptor = Gem::Specification.find_by_name(gem, version) |
184 rescue Exception |
185 gem_descriptor = nil |
186 end |
187 else |
188 gem_descriptor = Gem.source_index.find_name(gem, version)[0] |
189 end |
190 raise "Gem '#{gem}' was not found in version '#{version}'" if |
191 not gem_descriptor |
192 else |
193 if Gem::Specification.respond_to?(:find_by_name) |
194 begin |
195 gem_descriptor = Gem::Specification::find_by_name(gem) |
196 rescue Exception |
197 gem_descriptor = nil |
198 end |
199 else |
200 gem_descriptor = Gem.source_index.find_name(gem)[0] |
201 end |
202 raise "Gem '#{gem}' was not found" if not gem_descriptor |
203 end |
204 # get resource path |
205 gem_path = gem_descriptor.full_gem_path |
206 file_path = File.join(gem_path, path) |
207 return file_path |
208 end |
209 |
210 # Find a given template and return associated file. |
211 # - template: template to look for (like 'foo.bar'). |
212 # return: found associated file. |
213 def self.find_template(template) |
214 raise Bee::Util::BuildError.new("Invalid template name '#{template}'") if |
215 not template =~ /^([^.]+\.)?[^.]+$/ |
216 package, egg = template.split('.') |
217 if not egg |
218 egg = package |
219 package = 'bee' |
220 end |
221 resource = ":#{package}:egg/#{egg}.yml" |
222 begin |
223 file = absolute_path(resource, Dir.pwd) |
224 rescue Exception |
225 raise BuildError.new("Template '#{template}' not found") |
226 end |
227 raise BuildError.new("Template '#{template}' not found") if |
228 not File.exists?(file) |
229 return file |
230 end |
231 |
232 # Search files for a given templates that might contain a joker (*). |
233 # - template: template to look for ('foo.bar' or 'foo.*' or '*.bar'). |
234 # return: a hash associating template and corresponding file. |
235 def self.search_templates(template) |
236 raise Bee::Util::BuildError. |
237 new("Invalid template name '#{template}'") if |
238 not template =~ /^([^.]+\.)?[^.]+$/ |
239 package, egg = template.split('.') |
240 if not egg |
241 egg = package |
242 package = 'bee' |
243 end |
244 egg = '*' if egg == '?' |
245 resource = ":#{package}:egg/#{egg}.yml" |
246 begin |
247 glob = absolute_path(resource, nil) |
248 rescue |
249 if egg == '*' |
250 raise BuildError.new("Template package '#{package}' not found") |
251 else |
252 raise BuildError.new("Template '#{template}' not found") |
253 end |
254 end |
255 files = Dir.glob(glob) |
256 hash = {} |
257 for file in files |
258 egg = file.match(/.*?([^\/]+)\.yml/)[1] |
259 name = "#{package}.#{egg}" |
260 hash[name] = file |
261 end |
262 return hash |
263 end |
264 |
265 # Aliases |
266 |
267 def self.gem_available?(gem) |
268 return Bee::VersionDependant::gem_available?(gem) |
269 end |
270 |
271 def self.find_gems(*patterns) |
272 return Bee::VersionDependant::find_gems(*patterns) |
273 end |
274 |
275 # Class that holds information about a given method. |
276 class MethodInfo |
277 |
278 attr_accessor :source, :comment, :defn, :params |
279 |
280 # Constructor taking file name and line number. |
281 # - file: file name of the method. |
282 # - lineno: line number of the method. |
283 def initialize(file, lineno) |
284 lines = file_cache(file) |
285 @source = match_tabs(lines, lineno, "def") |
286 @comment = preceding_comment(lines, lineno) |
287 @defn = lines[lineno].strip.gsub(/^def\W+(.*)/){$1} |
288 if @defn =~ /.*?\(.*?\)/ |
289 @params = @defn.gsub(/.*?\((.*?)\)/){$1}.split(',').map{|p| p.strip} |
290 else |
291 @params = [] |
292 end |
293 end |
294 |
295 private |
296 |
297 @@file_cache = {} |
298 |
299 def file_cache(file) |
300 unless lines = @@file_cache[file] |
301 @@file_cache[file] = lines = File.new(file).readlines |
302 end |
303 lines |
304 end |
305 |
306 def match_tabs(lines, i, keyword) |
307 lines[i] =~ /(\W*)((#{keyword}(.*;\W*end)?)|(.*))/ |
308 return $2 if $4 or $5 |
309 tabs = $1 |
310 result = "" |
311 lines[i..-1].each do |line| |
312 result << line.gsub(/^#{tabs}(.*)/) { $1} |
313 return result if $1 =~ /^end/ |
314 end |
315 end |
316 |
317 def preceding_comment(lines, i) |
318 result = [] |
319 i = i-1 |
320 i = i-1 while lines[i] =~ /^\W*$/ |
321 if lines[i] =~ /^=end/ |
322 i = i-1 |
323 until lines[i] =~ /^=begin/ |
324 result.unshift lines[i] |
325 i = i-1 |
326 end |
327 else |
328 while lines[i] =~ /^\W*#(.*)/ |
329 result.unshift $1[1..-1] |
330 i = i-1 |
331 end |
332 end |
333 result.join("\n") |
334 end |
335 |
336 end |
337 |
338 # This abstract class provides information about its methods. |
339 class MethodInfoBase |
340 |
341 @@minfo = {} |
342 |
343 # Return comment for a given method. |
344 # - method: the method name to get info for. |
345 def self.method_info(method) |
346 @@minfo[method.to_s] |
347 end |
348 |
349 private |
350 |
351 # Called when a method is added. |
352 # - method: added method. |
353 def self.method_added(method) |
354 super if defined? super |
355 last = caller[0] |
356 file, lineno = last.match(/(.+?):(\d+)/)[1, 2] |
357 @@minfo[method.to_s] = MethodInfo.new(file, lineno.to_i - 1) |
358 end |
359 |
360 end |
361 |
362 # Error raised on a user error. This error should be raised to interrupt |
363 # the build with a message on console but with no stack trace (that should |
364 # be displayed on an internal error only). Include BuildErrorMixin to get |
365 # a convenient way to raise such an error. |
366 class BuildError < RuntimeError |
367 |
368 # Last met target. |
369 attr_accessor :target |
370 # Last met task. |
371 attr_accessor :task |
372 |
373 end |
374 |
375 # Build error mixin provides error() function to raise a BuildError. |
376 # Use this function to interrupt the build on a user error (bad YAML |
377 # syntax, error running a task and so on). This will result in an |
378 # error message on the console, with no stack trace. |
379 module BuildErrorMixin |
380 |
381 # Convenient method to raise a BuildError. |
382 # - message: error message. |
383 def error(message) |
384 Kernel.raise BuildError.new(message) |
385 end |
386 |
387 end |
388 |
389 # Mixin that provides a way to check a hash entries using a description |
390 # that associates hash keys with a :mandatory or :optional symbol. Other |
391 # keys are not allowed. |
392 module HashCheckerMixin |
393 |
394 include BuildErrorMixin |
395 |
396 # Check that all mandatory keys are in the hash and all keys in the |
397 # hash are in description. |
398 # - hash: hash to check. |
399 # - description: hash keys description. |
400 def check_hash(hash, description) |
401 # check for mandatory keys |
402 for key in description.keys |
403 case description[key] |
404 when :mandatory |
405 error "Missing mandatory key '#{key}'" if not hash.has_key?(key) |
406 when :optional |
407 else |
408 error "Unknown symbol '#{description[key]}'" |
409 end |
410 end |
411 # look for unknown keys in hash |
412 for key in hash.keys |
413 error "Unknown key '#{key}'" if not description.keys.member?(key) |
414 end |
415 end |
416 |
417 end |
418 |
419 end |
420 |
421 end |
Generated on Fri Oct 09 02:07:49 +0200 2015 with rcov 1.0.0