fit 1.1 → 1.2

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 (46) hide show
  1. data/CHANGELOG +27 -0
  2. data/{README.txt → README.rdoc} +28 -9
  3. data/Rakefile +44 -4
  4. data/bin/fit +29 -3
  5. data/doc/book/TestChatServer.html +207 -0
  6. data/doc/book/TestDiscount.html +178 -0
  7. data/doc/book/TestDiscountGroup.html +223 -0
  8. data/doc/book/TestDiscountMoney.html +179 -0
  9. data/doc/book/TestLateHours.html +245 -0
  10. data/doc/bugs/ColumnFixtureFollowedByActionFixture.html +316 -0
  11. data/doc/examples/MusicExampleWithErrors.html +1 -1
  12. data/doc/spec/annotation.html +3634 -3833
  13. data/doc/spec/index.html +1043 -947
  14. data/doc/spec/parse-windows-1252.html +3806 -0
  15. data/doc/spec/parse.html +3806 -3094
  16. data/doc/spec/ui.html +1537 -0
  17. data/lib/eg/all_files.rb +1 -1
  18. data/lib/eg/book/calculate_discount.rb +25 -0
  19. data/lib/eg/book/calculate_discount_money.rb +60 -0
  20. data/lib/eg/book/chat_server_actions.rb +85 -0
  21. data/lib/eg/book/discount_group_ordered_list.rb +59 -0
  22. data/lib/eg/book/rent/calculate_late_hours.rb +35 -0
  23. data/lib/eg/column_index.rb +4 -3
  24. data/lib/eg/music/display.rb +4 -2
  25. data/lib/eg/music/music.rb +4 -2
  26. data/lib/fat/command_line_fixture.rb +33 -0
  27. data/lib/fat/html_to_text_fixture.rb +3 -3
  28. data/lib/fit/column_fixture.rb +11 -7
  29. data/lib/fit/file_runner.rb +65 -10
  30. data/lib/fit/fixture.rb +10 -4
  31. data/lib/fit/fixture_loader.rb +29 -19
  32. data/lib/fit/parse.rb +46 -12
  33. data/lib/fit/type_adapter.rb +8 -3
  34. data/lib/fit/version.rb +26 -0
  35. data/lib/fitlibrary/comment_fixture.rb +12 -0
  36. data/lib/fitlibrary/spec/specify_fixture.rb +154 -0
  37. data/lib/fitlibrary/specify/column_fixture_under_test.rb +17 -0
  38. data/lib/fitlibrary/specify/column_fixture_under_test_with_args.rb +17 -0
  39. data/lib/fittask.rb +43 -47
  40. data/test/all_tests.rb +2 -1
  41. data/test/column_fixture_test.rb +26 -0
  42. data/test/file_runner_test.rb +2 -7
  43. data/test/fixture_loader_test.rb +16 -3
  44. data/test/parse_test.rb +18 -3
  45. data/test/type_adapter_test.rb +58 -44
  46. metadata +180 -156
@@ -18,7 +18,7 @@ module Eg
18
18
  cell.add_to_body Fit::Fixture.gray('no match')
19
19
  end
20
20
  end
21
- def expand pattern; Dir[pattern]; end
21
+ def expand pattern; Dir[pattern].sort; end
22
22
  protected
23
23
  def do_row_files row, files; do_files row, files; end
24
24
  def do_files row, files
@@ -0,0 +1,25 @@
1
+ require 'fit/column_fixture'
2
+
3
+ module Eg
4
+ module Book
5
+
6
+ class CalculateDiscount < Fit::ColumnFixture
7
+ attr_writer :amount
8
+ def initialize
9
+ @application = Discount.new
10
+ end
11
+ def discount
12
+ @application.get_discount @amount
13
+ end
14
+ end
15
+
16
+ class Discount
17
+ # A 5 percent discount is provided whenever the
18
+ # total purchase is greater than $1,000
19
+ def get_discount amount
20
+ (amount >= 1000) ? (amount / 100.0) * 5 : 0.0
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ require 'fit/column_fixture'
2
+
3
+ module Eg
4
+ module Book
5
+
6
+ class MoneyDiscount
7
+ # A 5 percent discount is provided whenever the
8
+ # total purchase is greater than $1,000
9
+ def get_discount amount
10
+ (amount >= Money.new(1000)) ? (amount * 0.05) : Money.new
11
+ end
12
+ end
13
+
14
+ class Money
15
+ attr_reader :cents
16
+ def initialize amount = 0
17
+ @cents = (amount * 100 + 0.5).to_i
18
+ end
19
+ def >= money
20
+ @cents >= money.cents
21
+ end
22
+ def * times
23
+ Money.new(@cents / 100.0 * times)
24
+ end
25
+ def == money
26
+ money.class == Money and @cents == money.cents
27
+ end
28
+ def Money.parse string
29
+ raise Exception.new('Invalid money value') unless string =~ /^\$/
30
+ dot = string.index '.'
31
+ raise Exception.new('Invalid money value') if dot.nil? or dot != string.size - 3
32
+ money = string[1..-1].to_f
33
+ Money.new money
34
+ end
35
+ def to_s
36
+ cent_string = "#{@cents % 100}"
37
+ cent_string += '0' if cent_string.size == 1
38
+ "$#{cents / 100}.#{cent_string}"
39
+ end
40
+ end
41
+
42
+ # Fixture
43
+
44
+ class CalculateDiscountMoney < Fit::ColumnFixture
45
+ attr_writer :amount
46
+ def initialize
47
+ @application = MoneyDiscount.new
48
+ end
49
+ def discount
50
+ @application.get_discount @amount
51
+ end
52
+ @@metadata = { 'amount' => Money, 'discount()' => Money }
53
+ def parse string, klass
54
+ return Money.parse(string) if klass == Money
55
+ super
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ require 'fit/fixture'
2
+
3
+ module Eg
4
+ module Book
5
+
6
+ class ChatServerActions < Fit::Fixture
7
+ def initialize
8
+ @chat = ChatRoom.new
9
+ end
10
+ def user user_name
11
+ @user_name = user_name
12
+ end
13
+ def connect
14
+ @chat.connect_user @user_name
15
+ end
16
+ def room room_name
17
+ @room_name = room_name
18
+ end
19
+ def new_room
20
+ @chat.user_creates_room @user_name, @room_name
21
+ end
22
+ def enter_room
23
+ @chat.user_enters_room @user_name, @room_name
24
+ end
25
+ def occupant_count
26
+ @chat.occupants @room_name
27
+ end
28
+ end
29
+
30
+ class ChatRoom
31
+ def initialize
32
+ @users = {}
33
+ @rooms = {}
34
+ end
35
+ def connect_user user_name
36
+ return false unless @users[user_name].nil?
37
+ @users[user_name] = User.new user_name
38
+ return true
39
+ end
40
+ def user_creates_room user_name, room_name
41
+ user = @users[user_name]
42
+ raise Exception.new("Unknown user name: #{user_name}") unless user
43
+ raise Exception.new("Duplicate room name: #{room_name}") if @rooms.include? room_name
44
+ @rooms[room_name] = Room.new room_name, user, self
45
+ end
46
+ def user_enters_room user_name, room_name
47
+ user = @users[user_name]
48
+ room = @rooms[room_name]
49
+ return false if user.nil? or room.nil?
50
+ room.add user
51
+ return true
52
+ end
53
+ def occupants room_name
54
+ room = @rooms[room_name]
55
+ raise Exception.new("Unknown room: #{room_name}") unless room
56
+ room.occupant_count
57
+ end
58
+ end
59
+
60
+ class User
61
+ attr_accessor :name
62
+ def initialize name
63
+ @name = name
64
+ end
65
+ end
66
+
67
+ require 'set'
68
+
69
+ class Room
70
+ def initialize room_name, owner, chat
71
+ @name = room_name
72
+ @owner = owner
73
+ @chat = chat
74
+ @users = Set.new
75
+ end
76
+ def add user
77
+ @users.add user
78
+ end
79
+ def occupant_count
80
+ @users.size
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ require 'fit/row_fixture'
2
+
3
+ module Eg
4
+ module Book
5
+
6
+ class DiscountGroupOrderedList < Fit::RowFixture
7
+ def query
8
+ groups = DiscountGroup.get_elements
9
+ ordered_groups = []
10
+ groups.each_with_index { |e, i|
11
+ group = OrderedDiscountGroup.new(i + 1, e.future_value,
12
+ e.max_owing, e.min_purchase, e.discount_percent)
13
+ ordered_groups << group
14
+ }
15
+ ordered_groups
16
+ end
17
+ def get_target_class
18
+ OrderedDiscountGroup
19
+ end
20
+ end
21
+
22
+ # System under test
23
+
24
+ class OrderedDiscountGroup
25
+ attr_reader :order, :future_value
26
+ attr_reader :max_owing, :min_purchase, :discount_percent
27
+ def initialize order, future_value, max_owing, min_purchase, discount_percent
28
+ @order = order
29
+ @future_value = future_value
30
+ @max_owing = max_owing
31
+ @min_purchase = min_purchase
32
+ @discount_percent = discount_percent
33
+ end
34
+ @@metadata = { 'order' => Fixnum, 'future_value' => String,
35
+ 'max_owing' => Float, 'min_purchase' => Float, 'discount_percent' => Float }
36
+ def OrderedDiscountGroup.metadata; @@metadata; end
37
+ end
38
+
39
+ class DiscountGroup
40
+ attr_reader :future_value
41
+ attr_reader :max_owing, :min_purchase, :discount_percent
42
+ def initialize future_value, max_owing, min_purchase, discount_percent
43
+ @future_value = future_value
44
+ @max_owing = max_owing
45
+ @min_purchase = min_purchase
46
+ @discount_percent = discount_percent
47
+ end
48
+ def DiscountGroup.get_elements
49
+ [ DiscountGroup.new('low', 0, 0, 0),
50
+ DiscountGroup.new('low', 0, 2000, 3),
51
+ DiscountGroup.new('medium', 500, 600, 3),
52
+ DiscountGroup.new('medium', 0, 500, 5),
53
+ DiscountGroup.new('high', 2000, 2000, 10)
54
+ ]
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ require 'fit/column_fixture'
2
+
3
+ module Eg
4
+ module Book
5
+
6
+ module Rent
7
+ class CalculateLateHours < Fit::ColumnFixture
8
+ attr_writer :hours_late, :grace, :high_demand
9
+ attr_writer :count_grace
10
+ def count_grace= string
11
+ @count_grace = { 'yes' => true, 'no' => false }[string]
12
+ end
13
+ def extra_hours
14
+ late_returns = LateReturns.new @count_grace
15
+ late_returns.extra_hours @hours_late, @grace, @high_demand
16
+ end
17
+ end
18
+ end
19
+
20
+ # System under test
21
+
22
+ class LateReturns
23
+ def initialize count_grace
24
+ @count_grace = count_grace
25
+ end
26
+ def extra_hours hours_late, grace, high_demand
27
+ return 0 if hours_late < 1
28
+ hours = @count_grace ? hours_late : (hours_late - grace)
29
+ return 0 if hours.zero?
30
+ high_demand + hours
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -31,8 +31,9 @@ module Eg
31
31
  # ...then find the columns in these classes
32
32
  columns = []
33
33
  names.each do |name|
34
- obj = Fit::FixtureLoader.new.find_class(name).new
35
- attributes = obj.methods - Object.new.methods
34
+ obj = @@loader.find_class(name).new
35
+ # Ruby 1.9 returns method names as symbols
36
+ attributes = (obj.methods - Object.new.methods).collect {|a| a.to_s}
36
37
  setters = attributes.dup.delete_if {|a| a[-1..-1] != "="}
37
38
  attributes -= setters
38
39
  setters.each do |s|
@@ -58,7 +59,7 @@ module Eg
58
59
  end
59
60
 
60
61
  def parse_class name
61
- Fit::FixtureLoader.new.find_class(name)
62
+ @@loader.find_class(name)
62
63
  end
63
64
 
64
65
  # Helper class
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'fit/row_fixture'
5
5
 
6
- require 'parsedate'
6
+ require 'date'
7
7
 
8
8
  module Eg
9
9
  module Music
@@ -13,7 +13,9 @@ module Eg
13
13
  def query; MusicLibrary.display_contents; end
14
14
  def parse string, klass
15
15
  if klass == Time
16
- Time.gm(*ParseDate.parsedate(string).compact)
16
+ d = Date._parse string
17
+ d_array = [d[:year], d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:zone], d[:wday]]
18
+ Time.gm(*d_array.compact)
17
19
  else
18
20
  super
19
21
  end
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2002 Cunningham & Cunningham, Inc.
2
2
  # Released under the terms of the GNU General Public License version 2 or later.
3
3
 
4
- require 'parsedate'
4
+ require 'date'
5
5
 
6
6
  module Eg
7
7
  module Music
@@ -48,7 +48,9 @@ module Eg
48
48
  m.track_number = data[6].to_i
49
49
  m.track_count = data[7].to_i
50
50
  m.year = data[8].to_i
51
- m.date = Time.gm(*ParseDate.parsedate(data[9]).compact)
51
+ d = Date._parse data[9]
52
+ d_array = [d[:year], d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:zone], d[:wday]]
53
+ m.date = Time.gm(*d_array.compact)
52
54
  m
53
55
  end
54
56
 
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2002 Cunningham & Cunningham, Inc.
2
+ # Released under the terms of the GNU General Public License version 2 or later.
3
+
4
+ require 'fit/column_fixture'
5
+
6
+ require 'optparse'
7
+
8
+ module Fat
9
+
10
+ # This is really testing OptionParser rather than some
11
+ # new RubyFIT code, but it's nice to have anyway just
12
+ # in case the Ruby standard library behaviour should
13
+ # change between subsequent versions.
14
+ class CommandLineFixture < Fit::ColumnFixture
15
+ attr_accessor :command_line
16
+ attr_reader :encoding
17
+ def initialize
18
+ @options = OptionParser.new
19
+ @options.on('--encoding=ENC') { |enc| @encoding = enc }
20
+ end
21
+ def input_file
22
+ args()[0]
23
+ end
24
+ def output_file
25
+ args()[1]
26
+ end
27
+ def args
28
+ @encoding = 'Implementation-specific'
29
+ @options.parse(command_line.split)
30
+ end
31
+ end
32
+
33
+ end
@@ -9,12 +9,12 @@ module Fat
9
9
  @@metadata = {'html' => String}
10
10
  attr_writer :html
11
11
  def text
12
- html = @html.gsub(/\\u00a0/, "\240")
12
+ html = @html.gsub(/\\u00a0/, [0x00a0].pack('U'))
13
13
  escape_ascii(Fit::Parse.html_to_text(html))
14
14
  end
15
15
  def escape_ascii text
16
- text.gsub("\x0a", "\\n").gsub("\x0d", "\\r").gsub("\xa0", " ")
16
+ text.gsub([0x0a].pack('U'), "\\n").gsub([0x0d].pack('U'), "\\r").gsub([0xa0].pack('U'), " ")
17
17
  end
18
18
  end
19
19
 
20
- end
20
+ end
@@ -79,13 +79,7 @@ module Fit
79
79
  if name.empty?
80
80
  @column_bindings << nil
81
81
  else
82
- suffix = ''
83
- if name =~ /\(\)$/
84
- suffix = '()'
85
- name = name[0...-2]
86
- end
87
- name = name.split(/([a-zA-Z][^A-Z]+)/).delete_if {|e| e.empty?}.collect {|e| e.downcase}.join('_')
88
- name += suffix
82
+ name = camel name
89
83
  adapter = TypeAdapter.for(self, name)
90
84
  adapter.type = get_target_class.metadata[name]
91
85
  @column_bindings << adapter
@@ -97,6 +91,16 @@ module Fit
97
91
  end
98
92
  end
99
93
 
94
+ def camel name
95
+ suffix = ''
96
+ if name =~ /\(\)$/
97
+ suffix = '()'
98
+ name = name[0...-2]
99
+ end
100
+ name = name.split(/([a-zA-Z][^A-Z]+)/).delete_if {|e| e.empty?}.collect {|e| e.strip.downcase}.join('_')
101
+ name += suffix
102
+ end
103
+
100
104
  def get_target_class; self.class; end
101
105
 
102
106
  end
@@ -5,17 +5,23 @@ require 'fit/fixture'
5
5
  require 'fit/parse'
6
6
 
7
7
  require 'fileutils' # for report generation
8
+ require 'iconv' # for I/O encoding
9
+ require 'stringio' # to help fix any possibly invalid byte sequence
8
10
 
9
11
  module Fit
10
12
 
11
13
  class FileRunner
12
14
  attr_accessor :input, :tables, :fixture, :output
13
15
 
16
+ DEFAULT_ENCODING = 'UTF-8'
17
+
14
18
  def initialize
15
19
  @fixture = Fixture.new
20
+ @encoding = DEFAULT_ENCODING
16
21
  end
17
22
 
18
- def run args
23
+ def run args, opts=nil
24
+ @encoding = opts[:encoding].upcase unless opts.nil?
19
25
  process_args args
20
26
  process
21
27
  $stderr.puts @fixture.totals
@@ -23,20 +29,58 @@ module Fit
23
29
  end
24
30
 
25
31
  def process_args args
26
- error "Usage: #{File.basename($0)} input_file output_file" unless args.size == 2
27
-
32
+ error "no input file" if args[0].nil?
28
33
  input_name = File.expand_path args[0]
29
- input_file = File.open input_name
30
- output_name = File.expand_path args[1]
31
- FileUtils.mkpath File.dirname(output_name)
32
- @output = File.open output_name, 'w'
34
+ begin
35
+ if input_name.respond_to? :encoding
36
+ input_file = File.open input_name, "r:#@encoding"
37
+ else
38
+ input_file = File.open input_name
39
+ end
40
+ rescue Errno::ENOENT
41
+ error "#{input_name}: file not found"
42
+ end
43
+ output_name = check_output_file args[1]
44
+ create_output_file output_name
33
45
  @fixture.summary['input file'] = input_name
34
46
  @fixture.summary['input update'] = input_file.mtime.to_s
35
47
  @fixture.summary['output file'] = output_name
36
- @input = input_file.read
48
+ unless input_name.respond_to? :encoding
49
+ read_file_with_encoding input_file
50
+ else
51
+ read_file input_file
52
+ end
37
53
  input_file.close
38
54
  end
39
55
 
56
+ def check_output_file arg
57
+ error "no output file" if arg.nil?
58
+ output_name = File.expand_path arg
59
+ end
60
+
61
+ def create_output_file output_name
62
+ FileUtils.mkpath File.dirname(output_name)
63
+ if output_name.respond_to? :encoding
64
+ @output = File.open output_name, "w:#@encoding"
65
+ else
66
+ @output = File.open output_name, 'w'
67
+ end
68
+ end
69
+
70
+ def read_file_with_encoding input_file
71
+ if @encoding == DEFAULT_ENCODING
72
+ @input = input_file.read
73
+ else
74
+ conv = Iconv.new DEFAULT_ENCODING, @encoding
75
+ @input = conv.iconv(input_file.read)
76
+ @input << conv.iconv(nil)
77
+ end
78
+ end
79
+
80
+ def read_file input_file
81
+ @input = (@encoding == DEFAULT_ENCODING) ? input_file.read : input_file.read.encode(DEFAULT_ENCODING)
82
+ end
83
+
40
84
  def process
41
85
  begin
42
86
  unless @input.index('<wiki>').nil?
@@ -49,7 +93,18 @@ module Fit
49
93
  rescue Exception => e
50
94
  exception e
51
95
  end
52
- @tables.print @output
96
+ if @encoding == DEFAULT_ENCODING
97
+ @tables.print @output
98
+ else
99
+ buffer = StringIO.new
100
+ conv = Iconv.new "#{DEFAULT_ENCODING}//IGNORE", DEFAULT_ENCODING
101
+ @tables.print buffer, conv
102
+ buffer.print conv.iconv(nil)
103
+ conv = Iconv.new "#@encoding//IGNORE", DEFAULT_ENCODING
104
+ @output.print conv.iconv(buffer.string)
105
+ @output << conv.iconv(nil)
106
+ buffer.close
107
+ end
53
108
  @output.close
54
109
  end
55
110
 
@@ -59,7 +114,7 @@ module Fit
59
114
  end
60
115
 
61
116
  def error msg
62
- $stderr.puts msg
117
+ $stderr.puts "#{File.basename($0)}: #{msg}"
63
118
  exit -1
64
119
  end
65
120