graffle 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/History.txt +5 -0
  2. data/Manifest.txt +22 -11
  3. data/README.txt +2 -2
  4. data/Rakefile.hoe +2 -1
  5. data/design-notes/graphical-tests-for-rails-objects.graffle +644 -0
  6. data/examples/objects with notes.expected +5 -0
  7. data/examples/objects with notes.graffle +338 -0
  8. data/examples/objects with notes.rb +42 -0
  9. data/examples/rails-workflow-test.expected +1 -1
  10. data/examples/rails-workflow-test.graffle/data.plist +86 -7
  11. data/examples/rails-workflow-test.rb +11 -9
  12. data/graffle.tmproj +82 -190
  13. data/lib/graffle.rb +19 -2
  14. data/lib/graffle/point.rb +1 -2
  15. data/lib/graffle/stereotypes.rb +62 -16
  16. data/lib/graffle/version.rb +1 -1
  17. data/lib/graphical_tests_for_rails.rb +8 -5
  18. data/lib/graphical_tests_for_rails/graphic-volunteers.rb +75 -0
  19. data/lib/graphical_tests_for_rails/orderings.rb +90 -84
  20. data/lib/graphical_tests_for_rails/picture-appliers.rb +225 -0
  21. data/lib/graphical_tests_for_rails/text-appliers.rb +135 -0
  22. data/lib/graphical_tests_for_rails/volunteer-pool.rb +115 -0
  23. data/test/abstract-graphic-tests.rb +48 -0
  24. data/test/document-tests.rb +5 -5
  25. data/test/examples-tests.rb +42 -0
  26. data/test/graphical_tests_for_rails/{graphic-interpreter-tests.rb → deprecated-graphic-interpreter-tests.rb} +11 -21
  27. data/test/graphical_tests_for_rails/graphic-volunteer-tests.rb +218 -0
  28. data/test/graphical_tests_for_rails/in-workflow-order-tests.rb +1 -1
  29. data/test/graphical_tests_for_rails/picture-applier-tests.rb +215 -0
  30. data/test/graphical_tests_for_rails/{text-interpreter-tests.rb → text-applier-tests.rb} +17 -3
  31. data/test/graphical_tests_for_rails/util.rb +16 -0
  32. data/test/line-graphic-tests.rb +9 -1
  33. data/test/note-tests.rb +62 -0
  34. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/data.plist +0 -0
  35. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image1.png +0 -0
  36. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image2.png +0 -0
  37. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image3.png +0 -0
  38. data/test/{graffle-file-types → sample-files}/multiple-canvases.graffle +0 -0
  39. data/test/{graffle-file-types → sample-files}/opening-tests.rb +9 -4
  40. data/test/{graffle-file-types → sample-files}/two-boxes-and-a-line.graffle +0 -0
  41. data/test/shaped-graphic-tests.rb +2 -3
  42. metadata +42 -18
  43. data/lib/graphical_tests_for_rails/interpreters.rb +0 -147
  44. data/test/tests-of-examples/workflow-slowtests.rb +0 -19
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'enumerator'
7
+ require 'test/unit/assertionfailederror'
8
+ require 'fluid'
9
+ require 'graphical_tests_for_rails/text-appliers'
10
+ require 'graphical_tests_for_rails/volunteer-pool'
11
+
12
+
13
+ module GraphicalTestsForRails
14
+
15
+ # A PictureApplier takes an array of graphics and arranges for
16
+ # messages derived from them to be sent to a _target_. PictureAppliers
17
+ # are "programmed" with other objects that handle the details of
18
+ # transforming graphics and the text they contain into messages.
19
+ #
20
+ # The
21
+ # graphics are processed in order. GraphicOrderer objects should be
22
+ # used to arrange the array before it's given to PictureApplier.
23
+ class PictureApplier
24
+ include Graffle
25
+ include GraphicalTestsForRails::TextAppliers
26
+
27
+ # The block tells the PictureApplier what to do by sending given,
28
+ # matching, use, and skip messages. That looks like this:
29
+ #
30
+ # PictureApplier.new {
31
+ # given('note').matching('ignore').skip
32
+ # given('shaped graphic content').use(ArgsFromQuotedText.new)
33
+ # }
34
+ #
35
+ # Each declaration sees if it can match a graphical object. If so,
36
+ # the last action is performed. Declarations are processed in
37
+ # order, so one match can be given precedence. Consider this:
38
+ #
39
+ # PictureApplier.new {
40
+ # given('note').matching('ignore').skip
41
+ # given('shaped graphic content').use(ArgsFromQuotedText.new)
42
+ # }
43
+ #
44
+ # This means that shaped graphic objects irrelevant to the test
45
+ # can be ignored by attaching the appropriate annotation.
46
+
47
+ def initialize(&block)
48
+ @universe = GraphicVolunteerPool.new
49
+ @mobilized = []
50
+ instance_eval(&block) if block
51
+ @mobilized << EagerButUselessVolunteer.new
52
+ end
53
+
54
+ # Claim that the PictureApplier matches graphics matching
55
+ # the _form_description_, a string. Here are the form descriptions
56
+ # currently supported:
57
+ #
58
+ # * 'shaped graphic content': A shaped graphic is a rectangle, oval,
59
+ # embedded picture, etc.: anything that is neither a line nor a
60
+ # group. The content is the text you type in when you double-click
61
+ # on the object. When given the form description, the PictureApplier
62
+ # will match any shaped graphic that contains text.
63
+ # * 'line label content': This matches any line that has a label.
64
+ # (Note: lines can have more than one label. That's not handled.)
65
+ # * 'note': Matches any object that contains an annotation. (Annotations are
66
+ # only available in OmniGraffle Pro. They're arbitrary text
67
+ # associated with a line or shaped graphic. You can see the
68
+ # annotations if you hover over the object.)
69
+ #
70
+ # The _form_description_ describes the first of two matching steps.
71
+ # If it matches, it extracts the appropriate text (content or
72
+ # annotation) and hands it to the next step, described by given.
73
+ #
74
+ # This method returns self so that other methods can be chained onto it.
75
+ def given(form_description)
76
+ @mobilized << @universe.mobilize_form_watcher(form_description)
77
+ self
78
+ end
79
+
80
+ # Claim that the PictureApplier matches particular text extracted from
81
+ # a graphical object. These _text_descriptions_ are currently supported:
82
+ #
83
+ # * 'text...': match if the text ends in ellipses (ignoring whitespace).
84
+ # * 'ignore': match if the text ends in the word "ignore" (ignoring whitespace).
85
+ #
86
+ # This method returns self so that other methods can be chained onto it.
87
+ def matching(text_description)
88
+ @mobilized.last.next = @universe.mobilize_content_watcher(text_description)
89
+ self
90
+ end
91
+
92
+ # When a PictureApplier matches an object, the extracted text is
93
+ # passed to the _text_applier_ to be turned into messages.
94
+ def use(text_applier)
95
+ @mobilized.last.text_applier = text_applier
96
+ end
97
+
98
+ # After a PictureApplier matches a graphical object, it's ignored.
99
+ # This is used to prevent text associated with the object from being
100
+ # turned into messages.
101
+ def skip
102
+ use(TextApplierThatDoesNothing.new)
103
+ end
104
+
105
+ # After the PictureApplier has been programmed, this method
106
+ # applies it to the _graphics_, resulting in methods that are sent
107
+ # to the _target_.
108
+ #
109
+ # If there's a problem, a log of what was sent to the _target_ is
110
+ # included in the exception message. The _log_ can be started out with
111
+ # the third argument, in which case new entries are appended. This can
112
+ # be useful when a single test is made up of several graphics lists.
113
+ def apply(graphics, target, log=[])
114
+ while_logging_for_test_errors(log) do
115
+ graphics.each { |g| find_volunteer_for(g).apply(g, target) }
116
+ end
117
+ end
118
+
119
+ # Given a _graphic_ find a GraphicVolunteer that can apply it.
120
+ # (Probably not for external use.)
121
+ def find_volunteer_for(graphic)
122
+ @mobilized.find { | m | m.likes?(graphic) }
123
+ end
124
+
125
+ private
126
+
127
+ def while_logging_for_test_errors(log)
128
+ Fluid.let(:log, log) do
129
+ begin
130
+ yield
131
+ rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
132
+ new_message = %Q{#{e.message} after\n#{Fluid.log.join("\n")}}
133
+ new_e = e.exception(new_message)
134
+ new_e.set_backtrace(e.backtrace)
135
+ raise new_e
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # This class is deprecated. See PictureApplier.
142
+ #
143
+ # A GraphicInterpreter takes a list of _graphics_, some
144
+ # instructions about how to interpret them, and a _target_.
145
+ # It interprets the instructions to create messages that it then
146
+ # sends to the target.
147
+ #
148
+ class GraphicInterpreter
149
+
150
+ # The _graphics_ are an array of OmniGraffle Graffle::AbstractGraphic objects. The
151
+ # _instructions_ map a string to an object that must respond to
152
+ # message_and_args_from. (See AbstractTextApplier#message_and_args_from
153
+ # for more.)
154
+ #
155
+ # Here are the available instructions (keys in _instructions_):
156
+ #
157
+ # * ['line labels' => ...]: each line within the (single)
158
+ # label on the line is to be interpreted as a message-send by an
159
+ # instance of the given class.
160
+ # * ['shaped graphic text' => ...]: each line of content
161
+ # within the graphic is to be interpreted by the given class. The
162
+ # content is the text you type in an object after double-clicking
163
+ # on it.
164
+ # * ['text...' => ...]: a line ending in ellipses is treated this way,
165
+ # no matter what other instructions say.
166
+ #
167
+ def initialize(graphics, instructions = {})
168
+ puts "Warning: #{self.class} is deprecated. Use PictureApplier instead."
169
+ @graphics = graphics
170
+ @line_label_strategy =
171
+ instructions['line labels']
172
+ @shaped_graphic_text_strategy =
173
+ instructions['shaped graphic text']
174
+
175
+ @ellipses_override = instructions['text...']
176
+ end
177
+
178
+ # Apply the _graphics_, according to the _instructions_, against
179
+ # the _target_.
180
+ def run_against(target)
181
+ @target = target
182
+ @log = []
183
+ @graphics.each do |g|
184
+ case g.stereotype.basename
185
+ when "ShapedGraphic"
186
+ process_commands(@shaped_graphic_text_strategy,
187
+ g.content.as_lines)
188
+ when "LineGraphic"
189
+ process_commands(@line_label_strategy,
190
+ g.label_lines)
191
+ end
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def process_commands(text_applier, command_lines)
198
+ return if text_applier.nil?
199
+ command_lines.each do |line|
200
+ info = if /\.\.\.\s*$/ =~ line && @ellipses_override
201
+ @ellipses_override.message_and_args_from(line)
202
+ else
203
+ text_applier.message_and_args_from(line)
204
+ end
205
+ begin
206
+ @log << readable(*info)
207
+ @target.send(*info)
208
+ rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
209
+ new_message = %Q{#{e.message} after\n#{@log.join("\n")}}
210
+ new_e = e.exception(new_message)
211
+ new_e.set_backtrace(e.backtrace)
212
+ raise new_e
213
+ end
214
+ end
215
+ end
216
+
217
+ def readable(message, *args)
218
+ inspected = args.collect { |a| a.inspect }
219
+ "+ #{message}(#{inspected.join(', ')})"
220
+ end
221
+
222
+ end
223
+
224
+ end
225
+
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ module GraphicalTestsForRails
7
+
8
+ # Just a wrapper module so that classes are grouped nicely in rdoc.
9
+ # You don't have to include this - that happens if you require
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
42
+
43
+
44
+ # The arguments to apply are quoted within the _string_.
45
+ # The message is the text before the first arg, snake-cased.
46
+ # For example, this:
47
+ # paul logs in with "john", password "j"
48
+ # means this:
49
+ # target.paul_logs_in_with("john", "j")
50
+ #
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
75
+ end
76
+
77
+ # Like ArgsFromQuotedText, but the first word (typically a name)
78
+ # is downcased and used as the first argument.
79
+ # For example, this:
80
+ # paul logs in with "john", password "j"
81
+ # means this:
82
+ # target.logs_in_with("paul", "john", "j")
83
+ #
84
+
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
+
96
+ end
97
+
98
+
99
+ # This AbstractTextApplier is created with the name of a message.
100
+ # When handed a string to apply, it downcases and strips the string,
101
+ # then uses it as the single argument to the method. For example, this:
102
+ # applier = TextAsNameOfPage.new("assert_on")
103
+ # applier.apply("HOME")
104
+ # turns into this:
105
+ # applier.assert_on("home")
106
+ class TextAsNameOfPage < AbstractTextApplier
107
+
108
+ # When producing a method call, objects of this class will
109
+ # prepend the _message_ to
110
+ # a single argument it constructs.
111
+ def initialize(message)
112
+ @message = message
113
+ 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
+ end
122
+
123
+ 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
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'graffle'
7
+ require 'graphical_tests_for_rails/graphic-volunteers'
8
+
9
+ # TODO: Turn these all into one class.
10
+ module GraphicalTestsForRails
11
+
12
+ # A GraphicVolunteerPool describes the ways a PictureApplier
13
+ # can by programmed with PictureApplier#given statements.
14
+ # You only care about this if you want to add new ways to use
15
+ # Graffle pictures in tests.
16
+
17
+ class GraphicVolunteerPool
18
+ def initialize
19
+ @graphic_maker = FormFocusedTemplate.new
20
+ @text_maker = ContentFocusedTemplate.new
21
+ end
22
+
23
+ def mobilize_form_watcher(description)
24
+ @graphic_maker.make_applier(description)
25
+ end
26
+
27
+ def mobilize_content_watcher(description)
28
+ @text_maker.make_applier(description)
29
+ end
30
+
31
+ end
32
+
33
+ # There are two distinct kinds of Templates only so they
34
+ # can produce different error messages.
35
+ class EmptyTemplate # :nodoc:
36
+ include Graffle
37
+
38
+ def initialize
39
+ @templates = {}
40
+ end
41
+
42
+ def proclaim(name)
43
+ prog1(GraphicVolunteer.new(name)) { | t | @templates[name] = t }
44
+ end
45
+
46
+ def make_applier(name)
47
+ t = @templates[name]
48
+ user_claims(t) {
49
+ "#{name} must be one of #{@templates.keys.inspect}"
50
+ }
51
+ t.dup
52
+ end
53
+ end
54
+
55
+ class FormFocusedTemplate < EmptyTemplate # :nodoc:
56
+ def initialize
57
+ super
58
+
59
+ proclaim('shaped graphic content').handles { | graphic |
60
+ shaped_graphic_with_content?(graphic)
61
+ }.and_extracts { | graphic |
62
+ graphic.content.as_lines
63
+ }
64
+
65
+ proclaim('line label content').handles { | graphic |
66
+ line_graphic_with_label_content?(graphic)
67
+ }.and_extracts { | graphic |
68
+ graphic.label_lines
69
+ }
70
+
71
+ proclaim('note').handles { | graphic |
72
+ graphic_with_note?(graphic)
73
+ }.and_extracts { | graphic |
74
+ graphic_note(graphic)
75
+ }
76
+ end
77
+
78
+ private
79
+ def labeled_line?(graphic)
80
+ return false unless graphic.behaves_like?(LineGraphic)
81
+ graphic.has_label?
82
+ end
83
+ def shaped_graphic_with_content?(graphic)
84
+ graphic.behaves_like?(ShapedGraphic) && graphic.has_content?
85
+ end
86
+
87
+ def line_graphic_with_label_content?(graphic)
88
+ labeled_line?(graphic) && graphic.label.has_content?
89
+ end
90
+
91
+ def graphic_with_note?(graphic)
92
+ return true if graphic.behaves_like?(AbstractGraphic) && graphic.has_note?
93
+ labeled_line?(graphic) && graphic.label.has_note?
94
+ end
95
+
96
+ def graphic_note(graphic)
97
+ return graphic.note.as_lines if graphic.has_note?
98
+ return graphic.label.note.as_lines
99
+ end
100
+ end
101
+
102
+ class ContentFocusedTemplate < EmptyTemplate # :nodoc:
103
+ def initialize
104
+ super
105
+
106
+ proclaim('text...').handles { | lines |
107
+ lines.join.strip =~ /\.\.+$/
108
+ }
109
+
110
+ proclaim('ignore').handles do | lines |
111
+ lines.any? { |l| l.strip.ends_with?("ignore") }
112
+ end
113
+ end
114
+ end
115
+ end