C0 code coverage information

Generated on Tue Oct 16 11:40:49 -0400 2007 with rcov 0.8.0


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.
Name Total lines Lines of code Total coverage Code coverage
lib/alexandria/library.rb 685 500
61.5% 
52.2% 
  1 # Copyright (C) 2004-2006 Laurent Sansonetti
  2 #
  3 # Alexandria is free software; you can redistribute it and/or
  4 # modify it under the terms of the GNU General Public License as
  5 # published by the Free Software Foundation; either version 2 of the
  6 # License, or (at your option) any later version.
  7 #
  8 # Alexandria is distributed in the hope that it will be useful,
  9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 11 # General Public License for more details.
 12 #
 13 # You should have received a copy of the GNU General Public
 14 # License along with Alexandria; see the file COPYING.  If not,
 15 # write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 16 # Boston, MA 02111-1307, USA.
 17 
 18 require 'yaml'
 19 require 'fileutils'
 20 require 'rexml/document'
 21 require 'tempfile'
 22 require 'etc'
 23 require 'open-uri'
 24 require 'observer'
 25 require 'singleton'
 26 
 27 class Array
 28   def sum
 29     self.inject(0) { |a, b| a + b }
 30   end
 31 end
 32 
 33 module Alexandria
 34   class Library < Array
 35       include Logging
 36 
 37     attr_reader :name
 38     attr_accessor :ruined_books
 39     DIR = File.join(ENV['HOME'], '.alexandria')
 40     EXT = { :book => '.yaml', :cover => '.cover' }
 41 
 42     include GetText
 43     extend GetText
 44     bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
 45 
 46     BOOK_ADDED, BOOK_UPDATED, BOOK_REMOVED = (0..3).to_a
 47     include Observable
 48 
 49     def path
 50       File.join(DIR, @name)
 51     end
 52 
 53     def self.generate_new_name(existing_libraries,
 54                                from_base=_("Untitled"))
 55       i = 1
 56       name = nil
 57       all_libraries = existing_libraries + @@deleted_libraries
 58       while true do
 59         name = i == 1 ? from_base : from_base + " #{i}"
 60         break unless all_libraries.find { |x| x.name == name }
 61         i += 1
 62       end
 63       return name
 64     end
 65 
 66     FIX_BIGNUM_REGEX =
 67       /loaned_since:\s*(\!ruby\/object\:Bignum\s*)?(\d+)\n/
 68 
 69       def self.load(name)
 70         test = [0,nil]
 71         ruined_books = []
 72         library = Library.new(name)
 73         FileUtils.mkdir_p(library.path) unless File.exists?(library.path)
 74         Dir.chdir(library.path) do
 75           Dir["*" + EXT[:book]].each do |filename|
 76 
 77             test[1] = filename if test[0] == 0
 78 
 79             #puts "back from the future of #{test[1]}:" if test[0] == 1
 80             #puts "Regularizing book :#{test[1]}" if $DEBUG
 81                   if not File.size? test[1]
 82                       log.warn { "Book file #{test[1]} was empty"}
 83                       next
 84                   end
 85             book = self.regularize_book_from_yaml(test[1])
 86             #puts "File state for #{test[1].inspect}: " + book.to_yaml if test[0] == 1
 87 
 88             old_isbn = book.isbn
 89             old_pub_year = book.publishing_year
 90             begin
 91               #puts "Entering resave-test block for #{test[1]}" if $DEBUG
 92               begin
 93                 book.isbn = self.canonicalise_ean(book.isbn).to_s unless book.isbn == nil
 94                 raise "Not a book: #{text.inspect}" unless book.is_a?(Book)
 95               rescue InvalidISBNError => e
 96                 #puts e.message if $DEBUG
 97                 book.isbn = old_isbn
 98               end
 99 
100               book.publishing_year = book.publishing_year.to_i unless book.publishing_year == nil
101 
102               # Or if isbn has changed
103               raise "#{test[1]} isbn is not okay" unless book.isbn == old_isbn
104 
105               # Re-save book if VERSION changes
106               raise "#{test[1]} version is not okay" unless book.version == VERSION
107 
108               # Or if publishing year has changed
109               raise "#{test[1]} pub year is not okay" unless book.publishing_year == old_pub_year
110 
111               # ruined_books << [book, book.isbn, library]
112               library << book
113             rescue => e
114               #puts "I'm reformatting #{test[1]} because #{e.message}"
115               book.version = VERSION
116               savedfilename = library.simple_save(book)
117               test[0] = test[0] + 1
118               test[1] = savedfilename
119               # retry ## no, this would retry the outer 'begin' block, not the Dir.each block
120 
121 
122               # retries the Dir.each block...
123               # but gives up after three tries
124               redo unless test[0] > 2
125 
126             else
127               test = [0,nil]
128             end
129           end
130 
131           # Since 0.4.0 the cover files '_small.jpg' and
132           # '_medium.jpg' have been deprecated for a single medium
133           # cover file named '.cover'.
134 
135           Dir["*" + '_medium.jpg'].each do |medium_cover|
136             begin
137               FileUtils.mv(medium_cover,
138                            medium_cover.sub(/_medium\.jpg$/,
139                                             EXT[:cover]))
140             rescue
141             end
142           end
143 
144 
145 
146           Dir["*" + EXT[:cover]].each do |cover|
147             md = /(.+)\.cover/.match(cover)
148             begin
149               ean = self.canonicalise_ean(md[1])
150             rescue
151               ean = md[1]
152             end
153             begin
154               FileUtils.mv(cover, ean + EXT[:cover]) unless cover == ean + EXT[:cover]
155             rescue
156             end
157           end
158 
159           FileUtils.rm_f(Dir['*_small.jpg'])
160         end
161         #puts ruined_books.inspect
162         library.ruined_books = ruined_books
163 
164         library
165       end
166 
167     def self.regularize_book_from_yaml(name)
168       text = IO.read(name)
169 
170       #Code to remove the mystery string in books imported from Amazon
171       # (In the past, still?) To allow ruby-amazon to be removed.
172 
173       # The string is removed on load, but can't make it stick, maybe has to do with cache
174 
175       if /!str:Amazon::Search::Response/.match(text)
176         #puts text if $DEBUG
177         text.gsub!("!str:Amazon::Search::Response", "")
178         #puts "got one!" if $DEBUG
179         #puts text if $DEBUG
180       end
181 
182       # Backward compatibility with versions <= 0.6.0, where the
183       # loaned_since field was a numeric.
184       if md = FIX_BIGNUM_REGEX.match(text)
185         new_yaml = Time.at(md[2].to_i).to_yaml
186         # Remove the "---" prefix.
187         new_yaml.sub!(/^\s*\-+\s*/, '')
188         text.sub!(md[0], "loaned_since: #{new_yaml}\n")
189       end
190       book = YAML.load(text)
191       unless book.isbn.class == String
192         # HACK
193         md = /isbn: (.+)/.match(text)
194         if md
195           string_isbn = md[1].strip
196           book.isbn = string_isbn
197           #puts "Reset to string value #{string_isbn}"
198         end
199       end
200       if book.isbn.class == String and book.isbn.length == 0
201         book.isbn = nil # save trouble later
202       end
203       book
204     end
205 
206     def self.loadall
207       a = []
208       begin
209         Dir.entries(DIR).each do |file|
210           # Skip hidden files.
211           next if /^\./.match(file)
212           # Skip non-directory files.
213           next unless File.stat(File.join(DIR, file)).directory?
214 
215           a << self.load(file)
216         end
217 
218       rescue Errno::ENOENT
219         FileUtils.mkdir_p(DIR)
220       end
221       # Create the default library if there is no library yet.
222 
223       if a.empty?
224         a << self.load(_("My Library"))
225       end
226 
227       return a
228     end
229 
230     def self.move(source_library, dest_library, *books)
231       dest = dest_library.path
232       books.each do |book|
233         FileUtils.mv(source_library.yaml(book), dest)
234         if File.exists?(source_library.cover(book))
235           FileUtils.mv(source_library.cover(book), dest)
236         end
237 
238         source_library.changed
239         source_library.old_delete(book)
240         source_library.notify_observers(source_library,
241                                         BOOK_REMOVED,
242                                         book)
243 
244         dest_library.changed
245         dest_library.delete_if { |book2| book2.ident == book.ident }
246         dest_library << book
247         dest_library.notify_observers(dest_library, BOOK_ADDED, book)
248       end
249     end
250 
251     class NoISBNError < StandardError
252       def initialize(msg)
253         super(msg)
254       end
255     end
256 
257     class InvalidISBNError < StandardError
258       attr_reader :isbn
259       def initialize(isbn=nil)
260         super()
261         @isbn = isbn
262       end
263     end
264 
265     def self.extract_numbers(isbn)
266       raise NoISBNError.new("Nil ISBN") if isbn == nil
267 
268       isbn.delete('- ').upcase.split('').map do |x|
269         raise InvalidISBNError.new(isbn) unless x =~ /[\dX]/
270           x == 'X' ? 10 : x.to_i
271       end
272     end
273 
274     def self.isbn_checksum(numbers)
275       sum = (0 ... numbers.length).inject(0) do |accumulator, i|
276         accumulator + numbers[i] * (i + 1)
277       end % 11
278 
279       sum == 10 ? 'X' : sum
280     end
281 
282     def self.valid_isbn?(isbn)
283       begin
284         numbers = self.extract_numbers(isbn)
285         numbers.length == 10 and self.isbn_checksum(numbers) == 0
286       rescue InvalidISBNError
287         false
288       end
289     end
290 
291     def self.ean_checksum(numbers)
292       (10 - ([1, 3, 5, 7, 9, 11].map { |x| numbers[x] }.sum * 3 +
293              [0, 2, 4, 6, 8, 10].map { |x| numbers[x] }.sum)) % 10
294     end
295 
296     def self.valid_ean?(ean)
297       begin
298         numbers = self.extract_numbers(ean)
299         (numbers.length == 13 and
300          self.ean_checksum(numbers[0 .. 11]) == numbers[12]) or
301          (numbers.length == 18 and
302           self.ean_checksum(numbers[0 .. 11]) == numbers[12])
303       rescue InvalidISBNError
304         false
305       end
306     end
307 
308     def self.upc_checksum(numbers)
309       (10 - ([0, 2, 4, 6, 8, 10].map { |x| numbers[x] }.sum * 3 +
310              [1, 3, 5, 7, 9].map { |x| numbers[x] }.sum)) % 10
311     end
312 
313     def self.valid_upc?(upc)
314       begin
315         numbers = self.extract_numbers(upc)
316         (numbers.length == 17 and
317          self.upc_checksum(numbers[0 .. 10]) == numbers[11])
318       rescue InvalidISBNError
319         false
320       end
321     end
322 
323     AMERICAN_UPC_LOOKUP = {
324             "014794" => "08041", "018926" => "0445", "02778" => "0449",
325             "037145" => "0812", "042799" => "0785",  "043144" => "0688",
326             "044903" => "0312", "045863" => "0517", "046594" => "0064",
327             "047132" => "0152", "051487" => "08167", "051488" => "0140",
328             "060771" => "0002", "065373" => "0373", "070992" => "0523",
329             "070993" => "0446", "070999" => "0345", "071001" => "0380",
330             "071009" => "0440", "071125" => "088677", "071136" => "0451",
331             "071149" => "0451", "071152" => "0515", "071162" => "0451",
332             "071268" => "08217", "071831" => "0425", "071842" => "08439",
333             "072742" => "0441", "076714" => "0671", "076783" => "0553",
334             "076814" => "0449", "078021" => "0872", "079808" => "0394",
335             "090129" => "0679", "099455" => "0061", "099769" => "0451"
336     }
337 
338     def self.upc_convert(upc)
339       test_upc = upc.map { |x| x.to_s }.join()
340       self.extract_numbers(AMERICAN_UPC_LOOKUP[test_upc])
341     end
342 
343     def self.canonicalise_ean(code)
344       code = code.to_s.delete('- ')
345       if self.valid_ean?(code)
346         return code
347       elsif self.valid_isbn?(code)
348         code = "978" + code[0..8]
349         return code + String( self.ean_checksum( self.extract_numbers( code ) ) )
350       elsif self.valid_upc?(code)
351         raise "fix function Alexandria::Library.canonicalise_ean"
352       else
353         raise InvalidISBNError.new(code)
354       end
355     end
356 
357     def self.canonicalise_isbn(isbn)
358       numbers = self.extract_numbers(isbn)
359       if self.valid_ean?(isbn)  and numbers[0 .. 2] != [9,7,8]
360         return isbn
361       end
362       canonical = if self.valid_ean?(isbn)
363                     # Looks like an EAN number -- extract the intersting part and
364                     # calculate a checksum. It would be nice if we could validate
365                     # the EAN number somehow.
366                     numbers[3 .. 11] + [self.isbn_checksum(numbers[3 .. 11])]
367                   elsif self.valid_upc?(isbn)
368                     # Seems to be a valid UPC number.
369                     prefix = self.upc_convert(numbers[0 .. 5])
370                     isbn_sans_chcksm = prefix + numbers[(8 + prefix.length) .. 17]
371                     isbn_sans_chcksm + [self.isbn_checksum(isbn_sans_chcksm)]
372                   elsif self.valid_isbn?(isbn)
373                     # Seems to be a valid ISBN number.
374                     numbers[0 .. -2] + [self.isbn_checksum(numbers[0 .. -2])]
375                   else
376                     raise InvalidISBNError.new(isbn)
377                   end
378 
379       canonical.map { |x| x.to_s }.join()
380     end
381 
382     def simple_save(book)
383       # Let's initialize the saved identifier if not already
384       # (backward compatibility from 0.4.0)
385       book.saved_ident ||= book.ident
386       if book.ident != book.saved_ident
387         #puts "Backwards compatibility step: #{book.saved_ident.inspect}, #{book.ident.inspect}" if $DEBUG
388         FileUtils.rm(yaml(book.saved_ident))
389       end
390       if File.exists?(cover(book.saved_ident))
391         begin
392           #puts "Moving cover #{cover(book.saved_ident)} to #{cover(book.ident)}" if $DEBUG
393           FileUtils.mv(cover(book.saved_ident), cover(book.ident))
394         rescue
395         end
396       end
397       book.saved_ident = book.ident
398 
399       filename = book.saved_ident.to_s + ".yaml"
400       #puts filename
401       File.open(filename, "w") { |io| io.puts book.to_yaml }
402       #puts "outputting book data..."
403       #puts File.open(filename, "r").read
404       filename
405     end
406 
407     def save(book, final=false)
408       changed unless final
409 
410       #puts "Saving book #{book.title}..." if $DEBUG
411 
412       # Let's initialize the saved identifier if not already
413       # (backward compatibility from 0.4.0).
414       book.saved_ident ||= book.ident
415 
416       #puts "#{book.title}'s saved_ident is #{book.saved_ident}" if $DEBUG
417 
418       if book.ident != book.saved_ident
419         FileUtils.rm(yaml(book.saved_ident))
420         if File.exists?(cover(book.saved_ident))
421           FileUtils.mv(cover(book.saved_ident), cover(book.ident))
422         end
423 
424         # Notify before updating the saved identifier, so the views
425         # can still use the old one to update their models.
426         notify_observers(self, BOOK_UPDATED, book) unless final
427         book.saved_ident = book.ident
428       end
429       already_there = (File.exists?(yaml(book)) and
430                        !@deleted_books.include?(book))
431 
432                        #puts "Doing the saving deed: #{book.title} -- #{book.isbn}" if $DEBUG
433                        File.open(yaml(book), "w") { |io| io.puts book.to_yaml }
434 
435                        # Do not notify twice.
436                        if changed?
437                          notify_observers(self,
438                                           already_there ? BOOK_UPDATED : BOOK_ADDED,
439                                           book)
440                        end
441     end
442 
443     def transport
444       config = Alexandria::Preferences.instance.http_proxy_config
445       config ? Net::HTTP.Proxy(*config) : Net::HTTP
446     end
447 
448     def save_cover(book, cover_uri)
449       Dir.chdir(self.path) do
450         # Fetch the cover picture.
451         cover_file = cover(book)
452         File.open(cover_file, "w") do |io|
453           uri = URI.parse(cover_uri)
454           if uri.scheme.nil?
455             # Regular filename.
456             File.open(cover_uri) { |io2| io.puts io2.read }
457           else
458             # Try open-uri.
459             io.puts transport.get(uri)
460           end
461         end
462 
463         # Remove the file if its blank.
464         if Alexandria::UI::Icons.blank?(cover_file)
465           File.delete(cover_file)
466         end
467       end
468     end
469 
470     @@deleted_libraries = []
471 
472     def self.deleted_libraries
473       @@deleted_libraries
474     end
475 
476     def self.really_delete_deleted_libraries
477       @@deleted_libraries.each do |library|
478         #puts "Deleting library directory (#{library.path})" if $DEBUG
479         FileUtils.rm_rf(library.path)
480       end
481     end
482 
483     def really_delete_deleted_books
484       @deleted_books.each do |book|
485         [yaml(book), cover(book)].each do |file|
486           #puts "Deleting book file #{file} " if $DEBUG
487           FileUtils.rm_f(file)
488         end
489       end
490     end
491 
492     alias_method :old_delete, :delete
493     def delete(book=nil)
494       if book.nil?
495         # Delete the whole library.
496         raise if @@deleted_libraries.include?(self)
497         @@deleted_libraries << self
498       else
499         if @deleted_books.include?(book)
500           doubles = @deleted_books.reject { |b| not b.equal? book }
501           raise "Book #{book.isbn} was already deleted" unless doubles.empty?
502         end
503         @deleted_books << book
504         i = self.index(book)
505         # We check object IDs there because the user could have added
506         # a book with the same identifier as another book he/she
507         # previously deleted and that he/she is trying to redo.
508         if i and self[i].equal? book
509           changed
510           old_delete(book) # FIX this will old_delete all '==' books
511           notify_observers(self, BOOK_REMOVED, book)
512         end
513       end
514     end
515 
516     def deleted?
517       @@deleted_libraries.include?(self)
518     end
519 
520     def undelete(book=nil)
521       if book.nil?
522         # Undelete the whole library.
523         raise unless @@deleted_libraries.include?(self)
524         @@deleted_libraries.delete(self)
525       else
526         raise unless @deleted_books.include?(book)
527         @deleted_books.delete(book)
528         unless self.include?(book)
529           changed
530           self << book
531           notify_observers(self, BOOK_ADDED, book)
532         end
533       end
534     end
535 
536     alias_method :old_select, :select
537     def select
538       filtered_library = Library.new(@name)
539       self.each do |book|
540         filtered_library << book if yield(book)
541       end
542       return filtered_library
543     end
544 
545     def cover(something)
546       ident = case something
547               when Book
548                 something.ident
549               when String
550                 something
551               when Bignum
552                 something
553               when Fixnum
554                 something
555               else
556                 raise "#{something} is a #{something.class}"
557               end
558       File.join(self.path, ident.to_s + EXT[:cover])
559     end
560 
561     def yaml(something, basedir=self.path)
562       ident = case something
563               when Book
564                 something.ident
565               when String
566                 something
567               when Bignum
568                 something
569               when Fixnum
570                 something
571               else
572                 raise "#{something} is #{something.class}"
573               end
574       File.join(basedir, ident.to_s + EXT[:book])
575     end
576 
577     def name=(name)
578       File.rename(path, File.join(DIR, name))
579       @name = name
580     end
581 
582     def n_rated
583       select { |x| !x.rating.nil? and x.rating > 0 }.length
584     end
585 
586     def n_unrated
587       length - n_rated
588     end
589 
590     def ==(object)
591       object.is_a?(self.class) && object.name == self.name
592     end
593 
594     def copy_covers(somewhere)
595       FileUtils.rm_rf(somewhere) if File.exists?(somewhere)
596       FileUtils.mkdir(somewhere)
597       each do |book|
598         next unless File.exists?(cover(book))
599         FileUtils.cp(File.join(self.path, book.ident + EXT[:cover]),
600                      File.join(somewhere, final_cover(book)))
601       end
602     end
603 
604     def self.jpeg?(file)
605             'JFIF' == IO.read(file, 10)[6..9]
606     end
607 
608     def final_cover(book)
609       book.ident + (Library.jpeg?(cover(book)) ? '.jpg' : '.gif')
610     end
611 
612     #########
613     protected
614     #########
615 
616     def initialize(name)
617       @name = name
618       @deleted_books = []
619     end
620   end
621 
622   class Libraries
623     attr_reader :all_libraries, :ruined_books
624 
625     include Observable
626     include Singleton
627 
628     def reload
629       @all_libraries.clear
630       @all_libraries.concat(Library.loadall)
631       @all_libraries.concat(SmartLibrary.loadall)
632       ruined = []
633       last = []
634       all_regular_libraries.each {|library|
635         ruined += library.ruined_books
636       }
637       #puts ruined.inspect
638       @ruined_books = ruined
639     end
640 
641     def all_regular_libraries
642       @all_libraries.select { |x| x.is_a?(Library) }
643     end
644 
645     def all_smart_libraries
646       @all_libraries.select { |x| x.is_a?(SmartLibrary) }
647     end
648 
649     LIBRARY_ADDED, LIBRARY_REMOVED = 1, 2
650 
651     def add_library(library)
652       @all_libraries << library
653       notify(LIBRARY_ADDED, library)
654     end
655 
656     def remove_library(library)
657       @all_libraries.delete(library)
658       notify(LIBRARY_REMOVED, library)
659     end
660 
661     def really_delete_deleted_libraries
662       Library.really_delete_deleted_libraries
663       SmartLibrary.really_delete_deleted_libraries
664     end
665 
666     def really_save_all_books
667       all_regular_libraries.each do |library|
668         library.each {|book| library.save(book, true)}
669       end
670     end
671 
672     #######
673     private
674     #######
675 
676     def initialize
677       @all_libraries = []
678     end
679 
680     def notify(action, library)
681       changed
682       notify_observers(self, action, library)
683     end
684   end
685 end

Generated using the rcov code coverage analysis tool for Ruby version 0.8.0.

Valid XHTML 1.0! Valid CSS!