graffle 0.1.9 → 0.2.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 (42) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +15 -8
  3. data/Rakefile.hoe +1 -2
  4. data/bin/interpret-intentions.rb +51 -0
  5. data/examples/objects with notes.rb +1 -0
  6. data/examples/old-style-rails-workflow-test.expected +11 -0
  7. data/examples/old-style-rails-workflow-test.rb +43 -0
  8. data/examples/rails-workflow-test.rb +7 -6
  9. data/examples/sheet with key.expected +5 -0
  10. data/examples/sheet with key.graffle +0 -0
  11. data/examples/sheet with key.rb +38 -0
  12. data/graffle.tmproj +242 -75
  13. data/lib/graffle.rb +11 -2
  14. data/lib/graffle/nodoc/hacks.rb +1 -0
  15. data/lib/graffle/point.rb +1 -0
  16. data/lib/graffle/stereotypes.rb +34 -3
  17. data/lib/graffle/version.rb +3 -1
  18. data/lib/graphical_tests_for_rails.rb +11 -9
  19. data/lib/graphical_tests_for_rails/jumbled-string-canonicalizer.rb +117 -0
  20. data/lib/graphical_tests_for_rails/orderings.rb +1 -2
  21. data/lib/graphical_tests_for_rails/picture-applier.rb +257 -0
  22. data/lib/graphical_tests_for_rails/stereotype-extensions.rb +111 -0
  23. data/lib/graphical_tests_for_rails/text-appliers.rb +10 -84
  24. data/lib/graphical_tests_for_rails/user-intention.rb +250 -0
  25. data/test/abstract-graphic-tests.rb +10 -0
  26. data/test/container-tests.rb +29 -0
  27. data/test/graphical_tests_for_rails/jumbled-string-canonicalizer-tests.rb +174 -0
  28. data/test/graphical_tests_for_rails/new-style-picture-applier-tests.rb +286 -0
  29. data/test/graphical_tests_for_rails/old-style-picture-applier-tests.rb +129 -0
  30. data/test/graphical_tests_for_rails/user-intention-tests.rb +389 -0
  31. data/test/graphical_tests_for_rails/util.rb +9 -0
  32. data/test/sheet-tests.rb +21 -0
  33. data/test/text-tests.rb +6 -0
  34. metadata +25 -26
  35. data/design-notes/graphical-tests-for-rails-objects.graffle +0 -644
  36. data/lib/graphical_tests_for_rails/graphic-volunteers.rb +0 -75
  37. data/lib/graphical_tests_for_rails/picture-appliers.rb +0 -225
  38. data/lib/graphical_tests_for_rails/volunteer-pool.rb +0 -115
  39. data/test/graphical_tests_for_rails/deprecated-graphic-interpreter-tests.rb +0 -121
  40. data/test/graphical_tests_for_rails/graphic-volunteer-tests.rb +0 -218
  41. data/test/graphical_tests_for_rails/picture-applier-tests.rb +0 -215
  42. data/test/graphical_tests_for_rails/text-applier-tests.rb +0 -111
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-31.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ # These extensions to Graffle allow code here to give
7
+ # stereotyped names to visual thingees and generate method
8
+ # calls from them.
9
+ #
10
+ # TODO: Should these be moved into Graffle proper? On the one
11
+ # hand, some of them (esp. the queries) could be useful. On the other,
12
+ # they have weird names (especially in the more specific modules.)
13
+
14
+ # These methods are tested via the user-intention-tests.
15
+
16
+
17
+ require 'graffle/stereotypes'
18
+
19
+ module Graffle
20
+
21
+ module AbstractGraphic
22
+ # These are utilities of use when you're handed an
23
+ # abstract graphic and want to know something about it.
24
+
25
+ def is_labeled_line? # :nodoc:
26
+ return false unless behaves_like?(LineGraphic)
27
+ has_label?
28
+ end
29
+
30
+ def has_line_label_note? # :nodoc:
31
+ is_labeled_line? && label.has_note?
32
+ end
33
+
34
+ def has_line_label_content? # :nodoc:
35
+ is_labeled_line? && label.has_content?
36
+ end
37
+
38
+ def has_line_note? # :nodoc:
39
+ behaves_like?(LineGraphic) && has_note?
40
+ end
41
+
42
+ def has_shaped_graphic_note? # :nodoc:
43
+ behaves_like?(ShapedGraphic) && has_note?
44
+ end
45
+
46
+ def has_shaped_graphic_content? # :nodoc:
47
+ behaves_like?(ShapedGraphic) && has_content?
48
+ end
49
+
50
+ def has_any_content? # :nodoc:
51
+ return true if behaves_like?(ShapedGraphic) && has_content?
52
+ has_line_label_content?
53
+ end
54
+
55
+ def has_any_note? # :nodoc:
56
+ return true if has_note?
57
+ has_line_label_note?
58
+ end
59
+
60
+
61
+ def any_content_lines # :nodoc:
62
+ if has_shaped_graphic_content?
63
+ shaped_graphic_content_lines
64
+ elsif has_line_label_content?
65
+ line_label_content_lines
66
+ else
67
+ user_is_bewildered("#{self.inspect} doesn't have content.")
68
+ end
69
+ end
70
+
71
+
72
+ def any_note_lines # :nodoc:
73
+ if has_shaped_graphic_note?
74
+ shaped_graphic_note_lines
75
+ elsif has_line_note?
76
+ line_note_lines
77
+ elsif has_line_label_note?
78
+ line_label_note_lines
79
+ else
80
+ user_is_bewildered("#{self.inspect} doesn't have content.")
81
+ end
82
+ end
83
+ end
84
+
85
+ module ShapedGraphic
86
+ def shaped_graphic_note_lines # :nodoc: # can be automatically generated
87
+ note.as_lines
88
+ end
89
+
90
+ def shaped_graphic_content_lines # :nodoc: # can be automatically generated
91
+ content.as_lines
92
+ end
93
+ end
94
+
95
+ module LineGraphic
96
+ def line_note_lines # :nodoc: # can be called by generated code
97
+ note.as_lines
98
+ end
99
+
100
+ def line_label_content_lines # :nodoc # can be called by generated code
101
+ label.content.as_lines
102
+ end
103
+
104
+ def line_label_note_lines # :nodoc: # can be called by generated code
105
+ label.note.as_lines
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+
@@ -1,44 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
- #
3
- # Created by Brian Marick on 2007-07-21.
2
+ # Created by Brian Marick on 2007-07-23.
4
3
  # Copyright (c) 2007. All rights reserved.
4
+ "prevent the above from infecting rdoc"
5
5
 
6
6
  module GraphicalTestsForRails
7
7
 
8
8
  # Just a wrapper module so that classes are grouped nicely in rdoc.
9
9
  # You don't have to include this - that happens if you require
10
10
  # graphical_tests_for_rails.rb.
11
- module TextAppliers
12
-
13
- # This is the base class for objects that know how to convert text
14
- # into a series of messages and send them to some target.
15
- class AbstractTextApplier
16
-
17
- # apply is given an array of text (_lines_). It is to convert
18
- # that text into messages and send them to _target_.
19
- #
20
- def apply(lines, target)
21
- message_sends = lines.collect {|line| message_and_args_from(line) }
22
- message_sends.each do |m|
23
- Fluid.log << readable(*m)
24
- target.send(*m)
25
- end
26
- end
27
-
28
- # message_and_args_from is given a string that is derived from some
29
- # text. It must answer an array whose first element is a
30
- # message name and whose remaining elements are appropriate message
31
- # arguments. Must be defined in any subclass.
32
- def message_and_args_from; subclass_responsibility; end
33
-
34
- private
35
- def readable(message, *args)
36
- inspected = args.collect { |a| a.inspect }
37
- "+ #{message}(#{inspected.join(', ')})"
38
- end
39
-
40
-
41
- end
11
+ module TextAppliers # :nodoc: all
42
12
 
43
13
 
44
14
  # The arguments to apply are quoted within the _string_.
@@ -48,30 +18,7 @@ module GraphicalTestsForRails
48
18
  # means this:
49
19
  # target.paul_logs_in_with("john", "j")
50
20
  #
51
- class ArgsFromQuotedText < AbstractTextApplier
52
-
53
- def message_and_args_from(string)
54
- quote_marker = '%<<<==-'
55
-
56
- string = string.gsub(/(\w)'(\w)/, "#{quote_marker}\\1\\2#{quote_marker}")
57
- tokens = string.split(/['"],?/, allow_trailing_null_fields = -1)
58
- message = tokens[0].strip.downcase
59
- args = tokens[1..-1]
60
-
61
-
62
- message.gsub!(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1\\2")
63
- message.gsub!(/\s+/,'_')
64
- message.gsub!(/-/, '_')
65
- message.gsub!(/\W/, '')
66
-
67
-
68
- args = args.enum_slice(2).collect { |e| e[0] }
69
- args = args.collect do | arg |
70
- arg.gsub(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1'\\2")
71
- end
72
-
73
- [message] + args
74
- end
21
+ class ArgsFromQuotedText
75
22
  end
76
23
 
77
24
  # Like ArgsFromQuotedText, but the first word (typically a name)
@@ -82,17 +29,7 @@ module GraphicalTestsForRails
82
29
  # target.logs_in_with("paul", "john", "j")
83
30
  #
84
31
 
85
- class PrefixNameAndArgsFromQuotedText < ArgsFromQuotedText
86
- def message_and_args_from(string)
87
- user_claims(string =~ /^\s*(\w+)\s+(.*)$/) {
88
- "'#{string}' cannot be split into a name and an action."
89
- }
90
- name = $1.downcase
91
- rest = super($2)
92
- message = rest.shift
93
- [message, name, *rest]
94
- end
95
-
32
+ class PrefixNameAndArgsFromQuotedText
96
33
  end
97
34
 
98
35
 
@@ -103,33 +40,22 @@ module GraphicalTestsForRails
103
40
  # applier.apply("HOME")
104
41
  # turns into this:
105
42
  # applier.assert_on("home")
106
- class TextAsNameOfPage < AbstractTextApplier
43
+ class TextAsNameOfPage
107
44
 
108
45
  # When producing a method call, objects of this class will
109
46
  # prepend the _message_ to
110
47
  # a single argument it constructs.
111
48
  def initialize(message)
49
+ user_claims(message == 'assert_on_page') {
50
+ "At the moment, a TextAsNameOfPage must be given assert_on_page."
51
+ }
112
52
  @message = message
113
53
  end
114
-
115
- # All of the text in the _string_ is expected to be the name
116
- # of a page. It is downcased and treated as the single argument to
117
- # the message passed to +new+.
118
- def message_and_args_from(string)
119
- [@message, string.downcase.strip]
120
- end
121
54
  end
122
55
 
123
56
  class TextApplierThatDoesNothing # :nodoc:
124
- def message_and_args_from(string)
125
- [:object_id] # no_op
126
- end
127
-
128
- def apply(lines, target)
129
- # do nothing, don't even bother sending a no-op
130
- end
131
57
  end
132
-
58
+
133
59
  end
134
60
 
135
61
  end
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-31.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'enumerator'
7
+ require 'graphical_tests_for_rails/stereotype-extensions'
8
+ require 'graphical_tests_for_rails/jumbled-string-canonicalizer'
9
+
10
+ module GraphicalTestsForRails
11
+ class UserIntention # :nodoc:
12
+ include GraphicalTestsForRails
13
+
14
+ module Names
15
+ USER_MEANS_LINE_LABEL_NOTE = "line_label_note"
16
+ USER_MEANS_LINE_LABEL_CONTENT = "line_label_content"
17
+ USER_MEANS_LINE_NOTE = "line_note"
18
+ USER_MEANS_SHAPED_GRAPHIC_NOTE = "shaped_graphic_note"
19
+ USER_MEANS_SHAPED_GRAPHIC_CONTENT = "shaped_graphic_content"
20
+ USER_MEANS_ANY_CONTENT = "any_content"
21
+ USER_MEANS_ANY_NOTE = "any_note"
22
+
23
+ USER_MEANS_CONTAINS_IGNORE = "contains_ignore"
24
+
25
+ USER_MEANS_LINES_WITH_ONE_WORD = "line_with_one_word"
26
+ USER_MEANS_LINE_ENDS_WITH_ELLIPSES = "ends_with_ellipses"
27
+ USER_MEANS_REMAINING_LINES = "catch_all_for_lines"
28
+
29
+ USER_MEANS_ACTOR_ACTION_WITH_QUOTE_ARGS = "user_action_quote_args"
30
+ USER_MEANS_CLAIM_WITH_QUOTE_ARGS = "claim_quote_args"
31
+ USER_MEANS_PAGE_NAME = "page_name"
32
+ USER_MEANS_DO_NOTHING = "do_nothing"
33
+
34
+ end
35
+ include Names
36
+
37
+ DISAMBIGUATE_BY_WORD_COUNT = proc { |a, b|
38
+ # The 'compact' is needed to get rid of non-matching alternatives
39
+ # in something like /(foo)|(bar)/.
40
+ words = proc { | x | - x.raw_reason.compact.length }
41
+ words[a] <=> words[b]
42
+ }
43
+
44
+ LINE_LABEL_REGEXP = /\b(line)\s+(labels?)\b/
45
+ NOTE_REGEXP = /\b(notes?)|(annotations?)\b/
46
+ CONTENT_REGEXP = /\b(contents?)\b/
47
+ LINE_REGEXP = /\b(lines?)\b/
48
+ SHAPED_GRAPHIC_REGEXP = /\b(shaped\s+graphics?)|(shapes?)\b/
49
+
50
+ PLACE = {
51
+ [LINE_LABEL_REGEXP, NOTE_REGEXP] => USER_MEANS_LINE_LABEL_NOTE,
52
+ [LINE_LABEL_REGEXP, CONTENT_REGEXP] => USER_MEANS_LINE_LABEL_CONTENT,
53
+ [LINE_REGEXP, NOTE_REGEXP] => USER_MEANS_LINE_NOTE,
54
+ [SHAPED_GRAPHIC_REGEXP, NOTE_REGEXP] => USER_MEANS_SHAPED_GRAPHIC_NOTE,
55
+ [SHAPED_GRAPHIC_REGEXP, CONTENT_REGEXP] => USER_MEANS_SHAPED_GRAPHIC_CONTENT,
56
+ [CONTENT_REGEXP] => USER_MEANS_ANY_CONTENT,
57
+ [NOTE_REGEXP] => USER_MEANS_ANY_NOTE,
58
+ }
59
+ PLACER = JumbledStringCanonicalizer.new(PLACE) {
60
+ disambiguate &DISAMBIGUATE_BY_WORD_COUNT
61
+ no_match_message { | source | "'#{source}' does not refer to any graphic."}
62
+ }
63
+
64
+ TEXTS = {
65
+ [/\b(ignore)\b/] => USER_MEANS_CONTAINS_IGNORE,
66
+ }
67
+ TEXTER = JumbledStringCanonicalizer.new(TEXTS) {
68
+ default nil
69
+ }
70
+
71
+
72
+ QUOTED_ARGS_REGEXPS = [/\b(quote\w*)\b/, /\b(arg\w*)\b/]
73
+ ACTIONS = {
74
+ [/\b(skip)|(ignore)\b/] => USER_MEANS_DO_NOTHING,
75
+ [/\b(user)\s+(actions?)\b/] + QUOTED_ARGS_REGEXPS => USER_MEANS_ACTOR_ACTION_WITH_QUOTE_ARGS,
76
+ [/\b(claims?)\b/] + QUOTED_ARGS_REGEXPS => USER_MEANS_CLAIM_WITH_QUOTE_ARGS,
77
+ [/\b(names?)\b/, /\b(pages?)\b/] => USER_MEANS_PAGE_NAME,
78
+ }
79
+ ACTIONER = JumbledStringCanonicalizer.new(ACTIONS) {
80
+ no_match_message { | source | "'#{source}' doesn't say what should happen in the test."}
81
+ multiple_match_message { | source, pretty_reasons, raw_reasons |
82
+ pretty_reasons = pretty_reasons.sort_by { |r| r.length }
83
+ "'#{source}' implies conflicting instructions:\n" +
84
+ "One is from these words: #{pretty_reasons[0]}\n" +
85
+ pretty_reasons[1..-1].collect { |r| "Another is from these: #{r}" }.join("\n")
86
+ }
87
+
88
+ }
89
+
90
+ OTHER_KIND_REGEXP = '(\b(other)\s+(kinds?)\s+(of)\b)'
91
+ OTHERWISE_REGEXP = '(\botherwise\b)'
92
+ LINE_GUARDS = {
93
+ [/\b(with)\s+.*\b(one)\s+(word)\b/] => USER_MEANS_LINES_WITH_ONE_WORD,
94
+ [/\b(ellipses)\b/] => USER_MEANS_LINE_ENDS_WITH_ELLIPSES,
95
+ [/\b(text\.\.\.*)/] => USER_MEANS_LINE_ENDS_WITH_ELLIPSES,
96
+ [/#{OTHERWISE_REGEXP}|#{OTHER_KIND_REGEXP}/] => USER_MEANS_REMAINING_LINES,
97
+ }
98
+ GUARDER = JumbledStringCanonicalizer.new(LINE_GUARDS) {
99
+ default nil
100
+ }
101
+
102
+ attr_reader :place, :text, :action, :line_guard, :source
103
+
104
+ def to_s
105
+ retval = "'#{source}' produces:\n"
106
+ retval += "\t#{action.value} if #{place.value}"
107
+ retval += " && #{text.value}" if text.value
108
+ retval += " && #{line_guard.value}" if line_guard.value
109
+ retval
110
+ end
111
+ def inspect; to_s; end
112
+
113
+ def self.understand(instruction)
114
+ prog1(new) { | i | i.understand(instruction) }
115
+ end
116
+
117
+ def understand(instruction)
118
+ @source = instruction
119
+ understand_place(instruction)
120
+ understand_text(instruction)
121
+ understand_action(instruction)
122
+ understand_line_guard(instruction)
123
+ end
124
+
125
+ def understand_place(instruction)
126
+ @place = PLACER.canonicalize(instruction)
127
+ end
128
+
129
+ def understand_text(instruction)
130
+ @text = TEXTER.canonicalize(instruction)
131
+ end
132
+
133
+ def understand_action(instruction)
134
+ @action = ACTIONER.canonicalize(instruction)
135
+ end
136
+
137
+ def understand_line_guard(instruction)
138
+ @line_guard = GUARDER.canonicalize(instruction)
139
+ end
140
+
141
+ def applies_to_graphic?(graphic)
142
+ graphic.send('has_' + place.value + '?')
143
+ end
144
+
145
+ def lines_from(graphic)
146
+ lines = graphic.send(place.value + '_lines')
147
+ lines.collect { |l| l.strip }.find_all { |l| l != '' }
148
+ end
149
+
150
+ def applies_to_text?(lines)
151
+ return true if text.value.nil?
152
+ send(text.value + '?', lines)
153
+ end
154
+
155
+
156
+ def contains_ignore?(lines)
157
+ lines.any? { |l| l.strip.ends_with?("ignore") }
158
+ end
159
+
160
+ def applies_to_line?(line)
161
+ return true if line_guard.value.nil?
162
+ send(line_guard.value + '_acceptable?', line)
163
+ end
164
+
165
+ def line_with_one_word_acceptable?(line)
166
+ line.split(' ').length == 1
167
+ end
168
+
169
+ def ends_with_ellipses_acceptable?(line)
170
+ line.strip =~ /\.\.+$/
171
+ end
172
+
173
+ def catch_all_for_lines_acceptable?(line)
174
+ true
175
+ end
176
+
177
+ def apply(string, target, log = [])
178
+ Applier.new(target, log).send("apply_" + action.value, string)
179
+ end
180
+
181
+ class Applier
182
+ def initialize(target, log)
183
+ @target = target
184
+ @log = log
185
+ end
186
+
187
+ def do_it(*message_send)
188
+ @log << readable(*message_send)
189
+ @target.send(*message_send)
190
+ end
191
+
192
+
193
+ def apply_user_action_quote_args(string)
194
+ user_claims(string =~ /^\s*(\w+)\s+(.*)$/) {
195
+ "'#{string}' cannot be split into a name and an action."
196
+ }
197
+ name = $1.downcase
198
+ rest = message_and_quote_args($2)
199
+ message = rest.shift
200
+ do_it(message, name, *rest)
201
+ end
202
+
203
+
204
+ def apply_claim_quote_args(string)
205
+ do_it(*message_and_quote_args(string))
206
+ end
207
+
208
+ def apply_page_name(string)
209
+ do_it('assert_on_page', string.downcase.strip)
210
+ end
211
+
212
+ def apply_do_nothing(string)
213
+ @log << "# '#{string}' ignored."
214
+ end
215
+
216
+ private
217
+ def message_and_quote_args(string)
218
+ quote_marker = '%<<<==-'
219
+
220
+ string = string.gsub(/(\w)'(\w)/, "#{quote_marker}\\1\\2#{quote_marker}")
221
+ tokens = string.split(/['"],?/, allow_trailing_null_fields = -1)
222
+ message = tokens[0].strip.downcase
223
+ args = tokens[1..-1]
224
+
225
+
226
+ message.gsub!(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1\\2")
227
+ message.gsub!(/\s+/,'_')
228
+ message.gsub!(/-/, '_')
229
+ message.gsub!(/\W/, '')
230
+
231
+
232
+ args = args.enum_slice(2).collect { |e| e[0] }
233
+ args = args.collect do | arg |
234
+ arg.gsub(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1'\\2")
235
+ end
236
+
237
+ [message] + args
238
+ end
239
+
240
+ def readable(message, *args)
241
+ inspected = args.collect { |a| a.inspect }
242
+ "+ #{message}(#{inspected.join(', ')})"
243
+ end
244
+
245
+
246
+
247
+
248
+ end
249
+ end
250
+ end