ape 1.0.0 → 1.5.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.
- data/LICENSE +1 -1
- data/README +22 -8
- data/Rakefile +66 -0
- data/bin/ape_server +3 -3
- data/lib/ape.rb +131 -937
- data/lib/ape/atomURI.rb +1 -1
- data/lib/ape/authent.rb +11 -17
- data/lib/ape/categories.rb +8 -7
- data/lib/ape/collection.rb +3 -7
- data/lib/ape/crumbs.rb +1 -1
- data/lib/ape/entry.rb +1 -1
- data/lib/ape/escaper.rb +1 -1
- data/lib/ape/feed.rb +26 -14
- data/lib/ape/handler.rb +8 -3
- data/lib/ape/html.rb +1 -1
- data/lib/ape/invoker.rb +1 -1
- data/lib/ape/invokers/deleter.rb +1 -1
- data/lib/ape/invokers/getter.rb +1 -1
- data/lib/ape/invokers/poster.rb +1 -1
- data/lib/ape/invokers/putter.rb +1 -1
- data/lib/ape/names.rb +1 -1
- data/lib/ape/print_writer.rb +4 -6
- data/lib/ape/reporter.rb +156 -0
- data/lib/ape/reporters/atom_reporter.rb +51 -0
- data/lib/ape/reporters/atom_template.eruby +38 -0
- data/lib/ape/reporters/html_reporter.rb +53 -0
- data/lib/ape/reporters/html_template.eruby +62 -0
- data/lib/ape/reporters/text_reporter.rb +37 -0
- data/lib/ape/samples.rb +30 -51
- data/lib/ape/server.rb +16 -4
- data/lib/ape/service.rb +1 -1
- data/lib/ape/util.rb +67 -0
- data/lib/ape/validator.rb +85 -57
- data/lib/ape/validator_dsl.rb +40 -0
- data/lib/ape/validators/entry_posts_validator.rb +226 -0
- data/lib/ape/validators/media_linkage_validator.rb +78 -0
- data/lib/ape/validators/media_posts_validator.rb +104 -0
- data/lib/ape/validators/sanitization_validator.rb +57 -0
- data/lib/ape/validators/schema_validator.rb +57 -0
- data/lib/ape/validators/service_document_validator.rb +64 -0
- data/lib/ape/validators/sorting_validator.rb +87 -0
- data/lib/ape/version.rb +1 -1
- data/{lib/ape/samples → samples}/atom_schema.txt +0 -0
- data/{lib/ape/samples → samples}/basic_entry.eruby +4 -4
- data/{lib/ape/samples → samples}/categories_schema.txt +0 -0
- data/{lib/ape/samples → samples}/mini_entry.eruby +0 -0
- data/{lib/ape/samples → samples}/service_schema.txt +0 -0
- data/{lib/ape/samples → samples}/unclean_xhtml_entry.eruby +0 -0
- data/test/test_helper.rb +33 -1
- data/test/unit/ape_test.rb +92 -0
- data/test/unit/authent_test.rb +2 -2
- data/test/unit/reporter_test.rb +102 -0
- data/test/unit/samples_test.rb +2 -2
- data/test/unit/validators_test.rb +50 -0
- data/{lib/ape/layout → web}/ape.css +5 -1
- data/{lib/ape/layout → web}/ape_logo.png +0 -0
- data/{lib/ape/layout → web}/index.html +2 -5
- data/{lib/ape/layout → web}/info.png +0 -0
- metadata +108 -56
- data/CHANGELOG +0 -1
- data/Manifest +0 -45
- data/ape.gemspec +0 -57
- data/scripts/go.rb +0 -29
data/LICENSE
CHANGED
data/README
CHANGED
@@ -1,12 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
== Atom Protocol Exerciser (APE)
|
2
|
+
|
3
|
+
APE is a sanity-checker for implementations of the Atom Publishing Protocol (AtomPub or APP). It is written in Ruby,
|
4
|
+
and provides a Mongrel-based HTML interface describing its interactions with the APP implementation under test.
|
5
|
+
|
6
|
+
For more information about the history and impetus for the creation of APE, see Tim Bray's account here[http://www.tbray.org/ongoing/When/200x/2006/08/11/Meet-the-Ape].
|
4
7
|
|
5
|
-
==
|
8
|
+
== License
|
9
|
+
|
10
|
+
Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved. See the included LICENSE[link:/files/LICENSE.html] file for details.
|
11
|
+
|
12
|
+
== Quick Start
|
13
|
+
|
14
|
+
Install APE via RubyGems:
|
15
|
+
|
16
|
+
$ gem install ape
|
17
|
+
|
18
|
+
Now, you should have the ape_server command available in your $PATH. Start the server with:
|
6
19
|
|
7
|
-
|
8
|
-
|
20
|
+
$ ape_server
|
21
|
+
|
22
|
+
This will start the server in the foreground. You can access APE in your browser at http://localhost:4000
|
9
23
|
|
10
|
-
==
|
24
|
+
== The Source
|
11
25
|
|
12
|
-
|
26
|
+
To access the latest source code for APE, see the project site at https://rubyforge.org/projects/ape
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
$:.unshift File.dirname(__FILE__) + '/lib'
|
7
|
+
require 'ape'
|
8
|
+
|
9
|
+
def spec
|
10
|
+
spec ||= Gem::Specification.new do |s|
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.name = 'ape'
|
13
|
+
s.version = Ape::VERSION::STRING
|
14
|
+
s.author = 'Tim Bray'
|
15
|
+
s.email = 'tim.bray@sun.com'
|
16
|
+
s.homepage = 'ape.rubyforge.org'
|
17
|
+
s.summary = 'The Atom Protocol Exerciser'
|
18
|
+
|
19
|
+
s.files = FileList['lib/**/*', 'samples/*', 'test/**/*', 'web/*',
|
20
|
+
'README', 'LICENSE', 'Rakefile'].to_ary
|
21
|
+
s.bindir = 'bin'
|
22
|
+
s.executable = 'ape_server'
|
23
|
+
|
24
|
+
s.has_rdoc = false
|
25
|
+
s.extra_rdoc_files = ['README', 'LICENSE']
|
26
|
+
|
27
|
+
s.rubyforge_project = 'ape'
|
28
|
+
|
29
|
+
s.add_dependency 'rake', '>= 0.8'
|
30
|
+
s.add_dependency 'mongrel', '>= 1.1.3'
|
31
|
+
s.add_dependency 'erubis', '>= 2.5.0'
|
32
|
+
s.add_dependency 'rubyforge', '>= 0.4'
|
33
|
+
s.add_dependency 'mocha', '>= 0.9.0'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def install_gem(*args)
|
38
|
+
cmd = []
|
39
|
+
cmd << "#{'sudo ' unless Gem.win_platform?}gem install"
|
40
|
+
sh cmd.push(*args.flatten).join(" ")
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Install the necessary dependencies'
|
44
|
+
task :setup do
|
45
|
+
installed = Gem::SourceIndex.from_installed_gems
|
46
|
+
dependencies = spec.dependencies
|
47
|
+
dependencies.select { |dep|
|
48
|
+
installed.search(dep.name, dep.version_requirements).empty? }.each do |dep|
|
49
|
+
puts "Installing #{dep} ..."
|
50
|
+
install_gem dep.name, "-v '#{dep.version_requirements.to_s}'"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The default task is run if rake is given no explicit arguments.
|
55
|
+
desc "Default Task"
|
56
|
+
task :default => :test
|
57
|
+
|
58
|
+
# Test Tasks ---------------------------------------------------------
|
59
|
+
|
60
|
+
desc "Run all tests"
|
61
|
+
task :test => [:test_units]
|
62
|
+
|
63
|
+
Rake::TestTask.new("test_units") do |t|
|
64
|
+
t.test_files = FileList['test/unit/*test.rb']
|
65
|
+
t.verbose = false
|
66
|
+
end
|
data/bin/ape_server
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# Copyright
|
2
|
+
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
3
3
|
# Use is subject to license terms - see file "LICENSE"
|
4
4
|
$:.unshift File.dirname(__FILE__) + '/../lib'
|
5
5
|
|
@@ -16,11 +16,11 @@ OPTIONS = {
|
|
16
16
|
}
|
17
17
|
|
18
18
|
parser = OptionParser.new do |opts|
|
19
|
-
opts.banner = '
|
19
|
+
opts.banner = 'The ape server options:'
|
20
20
|
opts.separator ''
|
21
21
|
opts.on('-a', '--address ADDRESS', 'Address to bind to', "default: #{OPTIONS[:host]}") { |v| OPTIONS[:host] = v }
|
22
22
|
opts.on('-p', '--port PORT', 'Port to bind to', "default: #{OPTIONS[:port]}") { |v| OPTIONS[:port] = v }
|
23
|
-
opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{Ape::
|
23
|
+
opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{Ape::Ape.home}") { |v| OPTIONS[:home] = v }
|
24
24
|
opts.on('-h', '--help', 'Displays this help') { puts opts; exit }
|
25
25
|
opts.parse!(ARGV)
|
26
26
|
end
|
data/lib/ape.rb
CHANGED
@@ -1,982 +1,176 @@
|
|
1
|
-
# Copyright
|
2
|
-
#
|
1
|
+
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# See the included LICENSE[link:/files/LICENSE.html] file for details.
|
3
3
|
$:.unshift File.dirname(__FILE__)
|
4
|
-
|
5
|
-
require 'rubygems'
|
6
|
-
require 'rexml/document'
|
7
|
-
require 'builder'
|
8
|
-
|
9
|
-
Dir[File.dirname(__FILE__) + '/ape/*.rb'].each { |l| require l }
|
10
|
-
|
11
4
|
module Ape
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
Dir[File.dirname(__FILE__) + '/ape/*.rb'].each { |l| require l }
|
8
|
+
|
9
|
+
@CONF = {}
|
10
|
+
|
11
|
+
def Ape.conf
|
12
|
+
@CONF
|
13
|
+
end
|
14
|
+
|
12
15
|
class Ape
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
raise ArgumentError, "output must be 'text' or 'html'"
|
20
|
-
end
|
21
|
-
|
22
|
-
@diarefs = {}
|
23
|
-
@dianum = 1
|
24
|
-
@@debugging = args[:debug]
|
25
|
-
@steps = []
|
26
|
-
@header = @footer = nil
|
27
|
-
@lnum = 1
|
28
|
-
@errors = @warnings = 0
|
16
|
+
attr_reader :aperc, :reporter
|
17
|
+
|
18
|
+
@@home = nil
|
19
|
+
def self.home=(home)
|
20
|
+
@@home = home
|
29
21
|
end
|
30
22
|
|
31
|
-
|
32
|
-
|
33
|
-
requested_e_coll = nil, requested_m_coll = nil)
|
34
|
-
|
35
|
-
# Google athent weirdness
|
36
|
-
@authent = Authent.new(username, password)
|
37
|
-
header(uri)
|
38
|
-
begin
|
39
|
-
might_fail(uri, requested_e_coll, requested_m_coll)
|
40
|
-
rescue Exception
|
41
|
-
error "Ouch! Ape fall down go boom; details: " +
|
42
|
-
"#{$!}\n#{$!.class}\n#{$!.backtrace}"
|
43
|
-
puts $!.backtrace.join("\n")
|
44
|
-
end
|
23
|
+
def self.home
|
24
|
+
@@home || ENV["APE_HOME"] || File.join(home_directory,".ape")
|
45
25
|
end
|
46
26
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
name = 'Retrieval of Service Document'
|
51
|
-
service = check_resource(uri, name, Names::AppMediaType)
|
52
|
-
return unless service
|
53
|
-
|
54
|
-
# * XML-parse the service doc
|
55
|
-
text = service.body
|
56
|
-
begin
|
57
|
-
service = REXML::Document.new(text, { :raw => nil })
|
58
|
-
rescue REXML::ParseException
|
59
|
-
prob = $!.to_s.gsub(/\n/, '<br/>')
|
60
|
-
error "Service document not well-formed: #{prob}"
|
61
|
-
return
|
62
|
-
end
|
63
|
-
|
64
|
-
# RNC-validate the service doc
|
65
|
-
Validator.validate(Samples.service_RNC, text, 'Service doc', self)
|
66
|
-
|
67
|
-
# * Do we have collections we can post an entry and a picture to?
|
68
|
-
# the requested_* arguments are the requested collection titles; if
|
69
|
-
# provided, try to match them, otherwise just pick the first listed
|
70
|
-
#
|
71
|
-
begin
|
72
|
-
collections = Service.collections(service, uri)
|
73
|
-
rescue Exception
|
74
|
-
error "Couldn't read collections from service doc: #{$!}"
|
75
|
-
return
|
76
|
-
end
|
77
|
-
entry_coll = media_coll = nil
|
78
|
-
if collections.length > 0
|
79
|
-
start_list "Found these collections"
|
80
|
-
collections.each do |collection|
|
81
|
-
list_item "'#{collection.title}' " +
|
82
|
-
"accepts #{collection.accept.join(', ')}"
|
83
|
-
if (!entry_coll) && collection.accept.index(Names::AtomEntryMediaType)
|
84
|
-
if requested_e_coll
|
85
|
-
if requested_e_coll == collection.title
|
86
|
-
entry_coll = collection
|
87
|
-
end
|
88
|
-
else
|
89
|
-
entry_coll = collection
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
if !media_coll
|
94
|
-
image_jpeg_ok = false
|
95
|
-
collection.accept.each do |types|
|
96
|
-
types.split(/, */).each do |type|
|
97
|
-
|
98
|
-
if type == '*/*' || type == 'image/*' || type == 'image/jpeg'
|
99
|
-
image_jpeg_ok = true
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
if image_jpeg_ok
|
104
|
-
if requested_m_coll
|
105
|
-
if requested_m_coll == collection.title
|
106
|
-
media_coll = collection
|
107
|
-
end
|
108
|
-
else
|
109
|
-
media_coll = collection
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
end_list
|
117
|
-
|
118
|
-
if entry_coll
|
119
|
-
info "Will use collection '#{entry_coll.title}' for entry creation."
|
120
|
-
test_entry_posts entry_coll
|
121
|
-
test_sorting entry_coll
|
122
|
-
test_sanitization entry_coll
|
123
|
-
else
|
124
|
-
warning "No collection for 'application/atom+xml;type=entry', won't test entry posting."
|
125
|
-
end
|
126
|
-
|
127
|
-
if media_coll
|
128
|
-
info "Will use collection '#{media_coll.title}' for media creation."
|
129
|
-
test_media_posts media_coll.href
|
130
|
-
test_media_linkage media_coll
|
131
|
-
else
|
132
|
-
warning "No collection for 'image/jpeg', won't test media posting."
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def test_media_linkage(coll)
|
137
|
-
info "TESTING: Media collection re-ordering after PUT."
|
138
|
-
|
139
|
-
# We'll post three mini entries to the collection
|
140
|
-
data = Samples.picture
|
141
|
-
poster = Poster.new(coll.href, @authent)
|
142
|
-
['One', 'Two', 'Three'].each do |num|
|
143
|
-
slug = "Picture #{num}"
|
144
|
-
poster.set_header('Slug', slug)
|
145
|
-
name = "Posting pic #{num}"
|
146
|
-
worked = poster.post('image/jpeg', data)
|
147
|
-
save_dialog(name, poster)
|
148
|
-
if !worked
|
149
|
-
error("Can't POST Picture #{num}: #{poster.last_error}", name)
|
150
|
-
return
|
151
|
-
end
|
152
|
-
sleep 2
|
153
|
-
end
|
154
|
-
|
155
|
-
# grab the collection to gather the MLE ids
|
156
|
-
entries = Feed.read(coll.href, 'Pictures from multi-post', self, true)
|
157
|
-
if entries.size < 3
|
158
|
-
error "Pictures apparently not in collection"
|
159
|
-
return
|
160
|
-
end
|
161
|
-
|
162
|
-
ids = entries.map { |e| e.child_content('id)') }
|
163
|
-
|
164
|
-
# let's update one of them; have to fetch it first to get the ETag
|
165
|
-
two_media = entries[1].link('edit-media')
|
166
|
-
if !two_media
|
167
|
-
error "Second entry from feed doesn't have an 'edit-media' link."
|
168
|
-
return
|
169
|
-
end
|
170
|
-
two_resp = check_resource(two_media, 'Fetch image to get ETag', 'image/jpeg', true)
|
171
|
-
unless two_resp
|
172
|
-
error "Can't fetch image to get ETag"
|
173
|
-
return
|
174
|
-
end
|
175
|
-
etag = two_resp.header 'etag'
|
176
|
-
|
177
|
-
putter = Putter.new(two_media, @authent)
|
178
|
-
putter.set_header('If-Match', etag)
|
179
|
-
|
180
|
-
name = 'Updating one of three pix with PUT'
|
181
|
-
if putter.put('image/jpeg', data)
|
182
|
-
good "Update one of newly posted pictures went OK."
|
183
|
-
else
|
184
|
-
save_dialog(name, putter)
|
185
|
-
error("Can't update picture at #{two_media}", name)
|
186
|
-
return
|
187
|
-
end
|
188
|
-
|
189
|
-
# now the order should have changed
|
190
|
-
wanted = [ ids[2], ids[0], ids[1] ]
|
191
|
-
entries = Feed.read(coll.href, 'MLEs post-update', self, true)
|
192
|
-
entries.each do |from_feed|
|
193
|
-
want = wanted.pop
|
194
|
-
unless from_feed.child_content('id').eql?(want)
|
195
|
-
error "Updating bits failed to re-order link entries in media collection."
|
196
|
-
return
|
197
|
-
end
|
198
|
-
|
199
|
-
# next to godliness
|
200
|
-
delete_entry(from_feed)
|
201
|
-
|
202
|
-
break if wanted.empty?
|
203
|
-
end
|
204
|
-
good "Entries correctly ordered after update of multi-post."
|
205
|
-
|
27
|
+
#recipe from cap
|
28
|
+
def self.home_directory
|
29
|
+
ENV["HOME"] || (ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") || "/"
|
206
30
|
end
|
207
31
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
return
|
218
|
-
end
|
219
|
-
|
220
|
-
location = poster.header('Location')
|
221
|
-
name = "Retrieval of unclean XHTML entry"
|
222
|
-
entry = check_resource(location, name, Names::AtomMediaType)
|
223
|
-
return unless entry
|
224
|
-
|
225
|
-
begin
|
226
|
-
entry = Entry.new(entry.body, location)
|
227
|
-
rescue REXML::ParseException
|
228
|
-
prob = $!.to_s.gsub(/\n/, '<br/>')
|
229
|
-
error "New entry is not well-formed: #{prob}"
|
230
|
-
return
|
231
|
-
end
|
232
|
-
|
233
|
-
no_problem = true
|
234
|
-
patterns = {
|
235
|
-
'//xhtml:script' => "Published entry retains xhtml:script element.",
|
236
|
-
'//*[@background]' => "Published entry retains 'background' attribute.",
|
237
|
-
'//*[@style]' => "Published entry retains 'style' attribute.",
|
238
|
-
|
239
|
-
}
|
240
|
-
patterns.each { |xp, message| warning(message) unless entry.xpath_match(xp).empty? }
|
241
|
-
|
242
|
-
entry.xpath_match('//xhtml:a').each do |a|
|
243
|
-
if a.attributes['href'] =~ /^([a-zA-Z]+):/
|
244
|
-
if $1 != 'http'
|
245
|
-
no_problem = false
|
246
|
-
warning "Published entry retains dangerous hyperlink: '#{a.attributes['href']}'."
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
delete_entry(entry)
|
252
|
-
|
253
|
-
good "Published entry appears to be sanitized." if no_problem
|
32
|
+
# Creates an Ape instance with options given in the +args+ Hash.
|
33
|
+
#
|
34
|
+
# ==== Options
|
35
|
+
# * :output - one of 'text' or 'html'. #report will output in this format. Defaults to 'html'.
|
36
|
+
# * :debug - enables debug information at each step in the output
|
37
|
+
def initialize(args = {})
|
38
|
+
output = args[:output] || 'html'
|
39
|
+
@reporter = Reporter.instance(output, args)
|
40
|
+
load File.join(Ape.home, 'aperc') if File.exist?(File.join(Ape.home, 'aperc'))
|
254
41
|
end
|
255
|
-
|
256
|
-
def test_sorting(coll)
|
257
|
-
|
258
|
-
info "TESTING: Collection re-ordering after PUT."
|
259
|
-
|
260
|
-
# We'll post three mini entries to the collection
|
261
|
-
poster = Poster.new(coll.href, @authent)
|
262
|
-
['One', 'Two', 'Three'].each do |num|
|
263
|
-
sleep 2
|
264
|
-
text = Samples.mini_entry.gsub('Mini-1', "Mini #{num}")
|
265
|
-
name = "Posting Mini #{num}"
|
266
|
-
worked = poster.post(Names::AtomEntryMediaType, text)
|
267
|
-
save_dialog(name, poster)
|
268
|
-
if !worked
|
269
|
-
error("Can't POST Mini #{name}: #{poster.last_error}", name)
|
270
|
-
return
|
271
|
-
end
|
272
|
-
end
|
273
42
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
two = from_feed if want == 'Mini Two'
|
285
|
-
break if wanted.empty?
|
286
|
-
end
|
287
|
-
good "Entries correctly ordered after multi-post."
|
288
|
-
|
289
|
-
# let's update one of them; have to fetch it first to get the ETag
|
290
|
-
link = two.link('edit', self)
|
291
|
-
unless link
|
292
|
-
error "Can't check entry without edit link, entry id: #{two.get_child('id/text()')}"
|
293
|
-
return
|
294
|
-
end
|
295
|
-
two_resp = check_resource(link, 'fetch two', Names::AtomMediaType, false)
|
296
|
-
|
297
|
-
correctly_ordered = false
|
298
|
-
if two_resp
|
299
|
-
etag = two_resp.header 'etag'
|
300
|
-
|
301
|
-
putter = Putter.new(link, @authent)
|
302
|
-
putter.set_header('If-Match', etag)
|
303
|
-
|
304
|
-
name = 'Updating mini-entry with PUT'
|
305
|
-
sleep 2
|
306
|
-
updated = two_resp.body.gsub('Mini Two', 'Mini-4')
|
307
|
-
unless putter.put(Names::AtomEntryMediaType, updated)
|
308
|
-
save_dialog(name, putter)
|
309
|
-
error("Can't update mini-entry at #{link}", name)
|
310
|
-
return
|
311
|
-
end
|
312
|
-
# now the order should have changed
|
313
|
-
wanted = ['Mini One', 'Mini Three', 'Mini-4']
|
314
|
-
correctly_ordered = true
|
315
|
-
else
|
316
|
-
error "Mini Two entry not received. Can't assure the correct order after update."
|
317
|
-
wanted = ['Mini One', 'Mini Two', 'Mini Three']
|
318
|
-
end
|
43
|
+
# Checks the AtomPub server at +uri+ for sanity.
|
44
|
+
#
|
45
|
+
# ==== Options
|
46
|
+
# * uri - the URI of the AtomPub server. Required.
|
47
|
+
# * username - an optional username for authentication
|
48
|
+
# * password - if a username is provided, a password is required. See Ape::Authent for more information.
|
49
|
+
# * requested_e_coll - a preferred entry collection to check
|
50
|
+
# * requested_m_coll - a preferred media collection to check
|
51
|
+
def check(uri, username=nil, password=nil,
|
52
|
+
requested_e_coll = nil, requested_m_coll = nil)
|
319
53
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
delete_entry(from_feed)
|
330
|
-
|
331
|
-
break if wanted.empty?
|
54
|
+
@authent = Authent.new(username, password)
|
55
|
+
reporter.header = uri
|
56
|
+
::Ape.conf[:REQUESTED_ENTRY_COLLECTION] = requested_e_coll if requested_e_coll
|
57
|
+
::Ape.conf[:REQUESTED_MEDIA_COLLECTION] = requested_m_coll if requested_m_coll
|
58
|
+
begin
|
59
|
+
might_fail(uri)
|
60
|
+
rescue Exception
|
61
|
+
reporter.error(self, "Ouch! Ape fall down go boom; details: " +
|
62
|
+
"#{$!}\n#{$!.class}\n#{$!.backtrace}")
|
332
63
|
end
|
333
|
-
good "Entries correctly ordered after update of multi-post." if correctly_ordered
|
334
|
-
|
335
64
|
end
|
336
65
|
|
337
|
-
def
|
66
|
+
def might_fail(uri)
|
67
|
+
service_validator = Validator.instance(:service_document, @reporter, @authent)
|
68
|
+
service_validator.validate(:uri => uri)
|
69
|
+
@service = service_validator.service_document
|
70
|
+
@entry_collections = service_validator.entry_collections
|
71
|
+
@media_collections = service_validator.media_collections
|
338
72
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
# * List the current entries, remember which IDs we've seen
|
343
|
-
info "TESTING: Entry-posting basics."
|
344
|
-
ids = []
|
345
|
-
unless entries.empty?
|
346
|
-
start_list "Now in the Entries feed"
|
347
|
-
entries.each do |entry|
|
348
|
-
list_item entry.summarize
|
349
|
-
ids << entry.child_content('id')
|
350
|
-
end
|
351
|
-
end_list
|
352
|
-
end
|
353
|
-
|
354
|
-
# Setting up to post a new entry
|
355
|
-
poster = Poster.new(collection_uri, @authent)
|
356
|
-
if poster.last_error
|
357
|
-
error("Unacceptable URI for '#{entry_collection.title}' collection: " +
|
358
|
-
poster.last_error)
|
359
|
-
return
|
360
|
-
end
|
361
|
-
|
362
|
-
my_entry = Entry.new(Samples.basic_entry)
|
363
|
-
|
364
|
-
# ask it to use this in the URI
|
365
|
-
slug_num = rand(100000)
|
366
|
-
slug = "ape-#{slug_num}"
|
367
|
-
slug_re = %r{ape.?#{slug_num}}
|
368
|
-
poster.set_header('Slug', slug)
|
369
|
-
|
370
|
-
# add some categories to the entry, and remember which
|
371
|
-
@cats = Categories.add_cats(my_entry, entry_collection, @authent, self)
|
372
|
-
|
373
|
-
# * OK, post it
|
374
|
-
worked = poster.post(Names::AtomEntryMediaType, my_entry.to_s)
|
375
|
-
name = 'Posting new entry'
|
376
|
-
save_dialog(name, poster)
|
377
|
-
if !worked
|
378
|
-
error("Can't POST new entry: #{poster.last_error}", name)
|
379
|
-
return
|
380
|
-
end
|
381
|
-
|
382
|
-
location = poster.header('Location')
|
383
|
-
unless location
|
384
|
-
error("No Location header upon POST creation", name)
|
385
|
-
return
|
386
|
-
end
|
387
|
-
good("Posting of new entry to the Entries collection " +
|
388
|
-
"reported success, Location: #{location}", name)
|
389
|
-
|
390
|
-
info "Examining the new entry as returned in the POST response"
|
391
|
-
check_new_entry(my_entry, poster.entry, "Returned entry") if poster.entry
|
392
|
-
|
393
|
-
# * See if the Location uri can be retrieved, and check its consistency
|
394
|
-
name = "Retrieval of newly created entry"
|
395
|
-
new_entry = check_resource(location, name, Names::AtomMediaType)
|
396
|
-
return unless new_entry
|
397
|
-
|
398
|
-
# Grab its etag
|
399
|
-
etag = new_entry.header 'etag'
|
400
|
-
|
401
|
-
info "Examining the new entry as retrieved using Location header in POST response:"
|
402
|
-
|
403
|
-
begin
|
404
|
-
new_entry = Entry.new(new_entry.body, location)
|
405
|
-
rescue REXML::ParseException
|
406
|
-
prob = $!.to_s.gsub(/\n/, '<br/>')
|
407
|
-
error "New entry is not well-formed: #{prob}"
|
408
|
-
return
|
409
|
-
end
|
410
|
-
|
411
|
-
# * See if the slug was used
|
412
|
-
slug_used = false
|
413
|
-
new_entry.alt_links.each do |a|
|
414
|
-
href = a.attributes['href']
|
415
|
-
if href && href.index(slug_re)
|
416
|
-
slug_used = true
|
73
|
+
if @entry_collections && true != ::Ape.conf[:ENTRY_VALIDATION_DISABLED]
|
74
|
+
[:entry_posts, :sorting, :sanitization].each do |option|
|
75
|
+
check_validator(option)
|
417
76
|
end
|
418
|
-
end
|
419
|
-
if slug_used
|
420
|
-
good "Client-provided slug '#{slug}' was used in server-generated URI."
|
421
77
|
else
|
422
|
-
warning "
|
423
|
-
end
|
424
|
-
|
425
|
-
check_new_entry(my_entry, new_entry, "Retrieved entry")
|
426
|
-
|
427
|
-
entry_id = new_entry.child_content('id')
|
428
|
-
|
429
|
-
# * fetch the feed again and check that version
|
430
|
-
from_feed = find_entry(collection_uri, "entry collection", entry_id)
|
431
|
-
if from_feed.class == String
|
432
|
-
good "About to check #{collection_uri}"
|
433
|
-
Feed.read(collection_uri, "Can't find entry in collection", self)
|
434
|
-
error "New entry didn't show up in the collections feed."
|
435
|
-
return
|
436
|
-
end
|
437
|
-
|
438
|
-
info "Examining the new entry as it appears in the collection feed:"
|
439
|
-
|
440
|
-
# * Check the entry from the feed
|
441
|
-
check_new_entry(my_entry, from_feed, "Entry from collection feed")
|
442
|
-
|
443
|
-
edit_uri = new_entry.link('edit', self)
|
444
|
-
if !edit_uri
|
445
|
-
error "Entry from Location header has no edit link."
|
446
|
-
return
|
78
|
+
reporter.warning(self, "No collection for 'application/atom+xml;type=entry', won't test entry posting.")
|
447
79
|
end
|
448
80
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
# Conditional PUT if an etag
|
454
|
-
putter.set_header('If-Match', etag) if etag
|
455
|
-
|
456
|
-
new_title = "Let’s all do the Ape!"
|
457
|
-
new_text = Samples.retitled_entry(new_title, entry_id)
|
458
|
-
response = putter.put(Names::AtomEntryMediaType, new_text)
|
459
|
-
save_dialog(name, putter)
|
460
|
-
|
461
|
-
if response
|
462
|
-
good("Update of new entry reported success.", name)
|
463
|
-
from_feed = find_entry(collection_uri, "entry collection", entry_id)
|
464
|
-
if from_feed.class == String
|
465
|
-
check_resource(collection_uri, "Check collection after lost update", nil, true)
|
466
|
-
error "Updated entry ID #{entry_id} not found in entries collection."
|
467
|
-
return
|
468
|
-
end
|
469
|
-
if from_feed.child_content('title') == new_title
|
470
|
-
good "Title of new entry successfully updated."
|
471
|
-
else
|
472
|
-
warning "After PUT update of title, Expected " +
|
473
|
-
"'#{new_title}', but saw '#{from_feed.child_content('title')}'"
|
81
|
+
if @media_collections && true != ::Ape.conf[:MEDIA_VALIDATION_DISABLED]
|
82
|
+
[:media_posts, :media_linkage].each do |option|
|
83
|
+
check_validator(option)
|
474
84
|
end
|
475
85
|
else
|
476
|
-
warning("
|
477
|
-
end
|
478
|
-
|
479
|
-
# the edit-uri might have changed
|
480
|
-
return unless delete_entry(from_feed, 'New Entry deletion')
|
481
|
-
|
482
|
-
# See if it's gone from the feed
|
483
|
-
still_there = find_entry(collection_uri, "entry collection", entry_id)
|
484
|
-
if still_there.class != String
|
485
|
-
error "Entry is still in collection post-deletion."
|
486
|
-
else
|
487
|
-
good "Entry not found in feed after deletion."
|
488
|
-
end
|
489
|
-
|
490
|
-
end
|
491
|
-
|
492
|
-
def test_media_posts media_collection
|
493
|
-
|
494
|
-
info "TESTING: Posting to media collection."
|
495
|
-
|
496
|
-
# * Post a picture to the media collection
|
497
|
-
#
|
498
|
-
poster = Poster.new(media_collection, @authent)
|
499
|
-
if poster.last_error
|
500
|
-
error("Unacceptable URI for '#{media_coll.title}' collection: " +
|
501
|
-
poster.last_error)
|
502
|
-
return
|
503
|
-
end
|
504
|
-
|
505
|
-
name = 'Post image to media collection'
|
506
|
-
|
507
|
-
# ask it to use this in the URI
|
508
|
-
slug_num = rand(100000)
|
509
|
-
slug = "apix-#{slug_num}"
|
510
|
-
slug_re = %r{apix.?#{slug_num}}
|
511
|
-
poster.set_header('Slug', slug)
|
512
|
-
|
513
|
-
#poster.set_header('Slug', slug)
|
514
|
-
worked = poster.post('image/jpeg', Samples.picture)
|
515
|
-
save_dialog(name, poster)
|
516
|
-
if !worked
|
517
|
-
error("Can't POST picture to media collection: #{poster.last_error}",
|
518
|
-
name)
|
519
|
-
return
|
86
|
+
reporter.warning(self, "No collection for 'image/jpeg', won't test media posting.")
|
520
87
|
end
|
521
|
-
|
522
|
-
good("Post of image file reported success, media link location: " +
|
523
|
-
"#{poster.header('Location')}", name)
|
524
88
|
|
525
|
-
#
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
return unless media_link_entry
|
530
|
-
|
531
|
-
if media_link_entry.last_error
|
532
|
-
error "Can't proceed with media-post testing."
|
533
|
-
return
|
534
|
-
end
|
535
|
-
|
536
|
-
# * See if the <content src= is there and usable
|
537
|
-
begin
|
538
|
-
media_link_entry = Entry.new(media_link_entry.body, mle_uri)
|
539
|
-
rescue REXML::ParseException
|
540
|
-
prob = $!.to_s.gsub(/\n/, '<br/>')
|
541
|
-
error "Media link entry is not well-formed: #{prob}"
|
542
|
-
return
|
543
|
-
end
|
544
|
-
content_src = media_link_entry.content_src
|
545
|
-
if (!content_src) || (content_src == "")
|
546
|
-
error "Media link entry has no content@src pointer to media resource."
|
547
|
-
return
|
89
|
+
#custom validators
|
90
|
+
Validator.custom_validators(@reporter, @authent).each do |validator|
|
91
|
+
opts = check_manifest(validator)
|
92
|
+
break if !validator.validate(opts) && validator.deterministic?
|
548
93
|
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def report(output=STDOUT)
|
97
|
+
@reporter.report(output)
|
98
|
+
end
|
549
99
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
end
|
100
|
+
def check_manifest(validator)
|
101
|
+
variables = {}
|
102
|
+
manifest = validator.manifest
|
103
|
+
variables[:service_doc] = @service if (manifest.include?(:service_doc))
|
104
|
+
manifest.delete(:service_doc)
|
556
105
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
return unless picture
|
562
|
-
|
563
|
-
if picture.body == Samples.picture
|
564
|
-
good "Media resource was apparently stored and retrieved properly."
|
565
|
-
else
|
566
|
-
warning "Media resource differs from posted picture"
|
567
|
-
end
|
568
|
-
|
569
|
-
# * Delete the media link entry
|
570
|
-
return unless delete_entry(media_link_entry, 'Deletion of media link entry')
|
571
|
-
|
572
|
-
# * media link entry still in feed?
|
573
|
-
still_there = find_entry(media_collection, "media collection", media_link_id)
|
574
|
-
if still_there.class != String
|
575
|
-
error "Media link entry is still in collection post-deletion."
|
576
|
-
else
|
577
|
-
good "Media link entry no longer in feed."
|
106
|
+
if (manifest.include?(:entry_collection))
|
107
|
+
variables[:entry_collection] = ::Ape.conf[:REQUESTED_ENTRY_COLLECTION].nil? ? @entry_collections.first :
|
108
|
+
get_collection(@entry_collections, ::Ape.conf[:REQUESTED_ENTRY_COLLECTION])
|
109
|
+
manifest.delete(:entry_collection)
|
578
110
|
end
|
579
111
|
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
else
|
585
|
-
good "Media resource no longer fetchable."
|
112
|
+
if (manifest.include?(:media_collection))
|
113
|
+
variables[:media_collection] = ::Ape.conf[:REQUESTED_MEDIA_COLLECTION].nil? ? @media_collections.first :
|
114
|
+
get_collection(@media_collections, ::Ape.conf[:REQUESTED_MEDIA_COLLECTION])
|
115
|
+
manifest.delete(:media_collection)
|
586
116
|
end
|
587
117
|
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
dc_subject = new_entry.child_content(Samples.foreign_child, Samples.foreign_namespace)
|
608
|
-
if dc_subject
|
609
|
-
if dc_subject == Samples.foreign_child_content
|
610
|
-
good "Server preserved foreign markup in #{desc}."
|
611
|
-
else
|
612
|
-
warning "Server altered content of foreign markup in #{desc}."
|
613
|
-
end
|
614
|
-
else
|
615
|
-
warning "Server discarded foreign markup in #{desc}."
|
616
|
-
end
|
617
|
-
end
|
618
|
-
|
619
|
-
#
|
620
|
-
# End of tests; support functions from here down
|
621
|
-
#
|
622
|
-
|
623
|
-
# Fetch a feed and look up an entry by ID in it
|
624
|
-
def find_entry(feed_uri, name, id, report=false)
|
625
|
-
entries = Feed.read(feed_uri, name, self, report)
|
626
|
-
entries.each do |from_feed|
|
627
|
-
return from_feed if id == from_feed.child_content('id')
|
628
|
-
end
|
629
|
-
|
630
|
-
return "Couldn't find id #{id} in feed #{feed_uri}"
|
631
|
-
end
|
632
|
-
|
633
|
-
# remember the dialogue that the get/put/post/delete actor recorded
|
634
|
-
def save_dialog(name, actor)
|
635
|
-
@dialogs[name] = actor.crumbs if @dialogs
|
636
|
-
end
|
637
|
-
|
638
|
-
# Get a resource, optionally check its content-type
|
639
|
-
def check_resource(uri, name, content_type, report=true)
|
640
|
-
resource = Getter.new(uri, @authent)
|
641
|
-
|
642
|
-
# * Check the URI
|
643
|
-
if resource.last_error
|
644
|
-
error("Unacceptable #{name} URI: " + resource.last_error, name) if report
|
645
|
-
return nil
|
646
|
-
end
|
647
|
-
|
648
|
-
# * Get it, make sure it has the right content-type
|
649
|
-
worked = resource.get(content_type)
|
650
|
-
@dialogs[name] = resource.crumbs if @dialogs
|
651
|
-
|
652
|
-
if (resource.security_warning and not @security_warning)
|
653
|
-
@security_warning = true
|
654
|
-
warning("Sending authentication information over a open channel is not a good security practice.", name)
|
655
|
-
end
|
656
|
-
|
657
|
-
if !worked
|
658
|
-
# oops, couldn't even get get it
|
659
|
-
error("#{name} failed: " + resource.last_error, name) if report
|
660
|
-
return nil
|
661
|
-
|
662
|
-
elsif resource.last_error
|
663
|
-
# oops, media-type problem
|
664
|
-
error("#{name}: #{resource.last_error}", name) if report
|
665
|
-
|
666
|
-
else
|
667
|
-
# resource fetched and is of right type
|
668
|
-
good("#{name}: it exists and is served properly.", name) if report
|
669
|
-
end
|
670
|
-
|
671
|
-
return resource
|
672
|
-
end
|
673
|
-
|
674
|
-
def header(uri)
|
675
|
-
@header = "APP Service doc: #{uri}"
|
676
|
-
end
|
677
|
-
|
678
|
-
def footer(message)
|
679
|
-
@footer = message
|
680
|
-
end
|
681
|
-
|
682
|
-
def show_crumbs key
|
683
|
-
@dialogs[key].each do |d|
|
684
|
-
puts "D: #{d}"
|
685
|
-
end
|
686
|
-
end
|
687
|
-
|
688
|
-
def warning(message, crumb_key=nil)
|
689
|
-
@warnings += 1
|
690
|
-
if @dialogs
|
691
|
-
step "D#{crumb_key}" if crumb_key
|
692
|
-
show_crumbs(crumb_key) if crumb_key && @@debugging
|
693
|
-
end
|
694
|
-
step "W" + message
|
695
|
-
end
|
696
|
-
|
697
|
-
def error(message, crumb_key=nil)
|
698
|
-
@errors += 1
|
699
|
-
if @dialogs
|
700
|
-
step "D#{crumb_key}" if crumb_key
|
701
|
-
show_crumbs(crumb_key) if crumb_key && @@debugging
|
702
|
-
end
|
703
|
-
step "E" + message
|
704
|
-
end
|
705
|
-
|
706
|
-
def good(message, crumb_key=nil)
|
707
|
-
if @dialogs
|
708
|
-
step "D#{crumb_key}" if crumb_key
|
709
|
-
show_crumbs(crumb_key) if crumb_key && @@debugging
|
710
|
-
end
|
711
|
-
step "G" + message
|
712
|
-
end
|
713
|
-
|
714
|
-
def info(message)
|
715
|
-
step "I" + message
|
716
|
-
end
|
717
|
-
|
718
|
-
def step(message)
|
719
|
-
puts "PROGRESS: #{message[1..-1]}" if @@debugging
|
720
|
-
@steps << message
|
721
|
-
end
|
722
|
-
|
723
|
-
def start_list(message)
|
724
|
-
step [ message + ":" ]
|
725
|
-
end
|
726
|
-
|
727
|
-
def list_item(message)
|
728
|
-
@steps[-1] << message
|
729
|
-
end
|
730
|
-
|
731
|
-
def end_list
|
732
|
-
end
|
733
|
-
|
734
|
-
def line
|
735
|
-
printf "%2d. ", @lnum
|
736
|
-
@lnum += 1
|
737
|
-
end
|
738
|
-
|
739
|
-
def report(output=STDOUT)
|
740
|
-
if @output == 'text'
|
741
|
-
report_text output
|
742
|
-
else
|
743
|
-
report_html output
|
744
|
-
end
|
745
|
-
end
|
746
|
-
|
747
|
-
def report_html(output=STDOUT)
|
748
|
-
dialog = nil
|
749
|
-
|
750
|
-
if output == STDOUT
|
751
|
-
output.puts "Status: 200 OK\r"
|
752
|
-
output.puts "Content-type: text/html; charset=utf-8\r"
|
753
|
-
output.puts "\r"
|
754
|
-
end
|
755
|
-
|
756
|
-
@w = Builder::XmlMarkup.new(:target => output)
|
757
|
-
@w.html do
|
758
|
-
@w.head do
|
759
|
-
@w.title { @w.text! 'Atom Protocol Exerciser Report' }
|
760
|
-
@w.text! "\n"
|
761
|
-
@w.link(:rel => 'stylesheet', :type => 'text/css',:href => '../ape/ape.css' )
|
762
|
-
end
|
763
|
-
@w.text! "\n"
|
764
|
-
@w.body do
|
765
|
-
@w.h2 { @w.text! 'The Ape says:' }
|
766
|
-
@w.text! "\n"
|
767
|
-
if @header
|
768
|
-
@w.p { @w.text! @header }
|
769
|
-
@w.p do
|
770
|
-
@w.text! "Summary: "
|
771
|
-
@w.text!((@errors == 1) ? '1 error, ' : "#{@errors} errors, ")
|
772
|
-
@w.text!((@warnings == 1) ? '1 warning.' : "#{@warnings} warnings.")
|
773
|
-
end
|
774
|
-
@w.text! "\n"
|
775
|
-
end
|
776
|
-
@w.ol do
|
777
|
-
@w.text! "\n"
|
778
|
-
@steps.each do |step|
|
779
|
-
if step.kind_of? Array
|
780
|
-
# it's a list; no dialog applies
|
781
|
-
@w.li do
|
782
|
-
@w.p do
|
783
|
-
write_mark :info
|
784
|
-
@w.text! " #{step[0]}\n"
|
785
|
-
end
|
786
|
-
@w.ul do
|
787
|
-
step[1 .. -1].each { |li| report_li(nil, nil, li) }
|
788
|
-
end
|
789
|
-
@w.text! "\n"
|
790
|
-
end
|
791
|
-
else
|
792
|
-
body = step[1 .. -1]
|
793
|
-
opcode = step[0,1]
|
794
|
-
if opcode == "D"
|
795
|
-
dialog = body
|
796
|
-
else
|
797
|
-
case opcode
|
798
|
-
when "W" then report_li(dialog, :question, body)
|
799
|
-
when "E" then report_li(dialog, :exclamation, body)
|
800
|
-
when "G" then report_li(dialog, :check, body)
|
801
|
-
when "I" then report_li(dialog, :info, body)
|
802
|
-
else
|
803
|
-
line
|
804
|
-
puts "HUH? #{step}"
|
118
|
+
manifest.each do |option|
|
119
|
+
if (option.instance_of?(Hash))
|
120
|
+
all_collections = @entry_collections + @media_collections
|
121
|
+
option.each do |key, value|
|
122
|
+
unless (value.instance_of?(Hash))
|
123
|
+
#request a collection by its title, i.e: :entry_collection => 'Posts'
|
124
|
+
variables[key] = get_collection(all_collections, value)
|
125
|
+
else
|
126
|
+
#request the first collection that matches the options,
|
127
|
+
# i.e: :entry_collection => {:accept => 'image/png'}
|
128
|
+
# :entry_collection => {:title => 'Atachments', :accept => 'video/*'}
|
129
|
+
hash = value
|
130
|
+
variables[key] = all_collections.select do |collection|
|
131
|
+
matches = nil
|
132
|
+
hash.each do |k, v|
|
133
|
+
begin
|
134
|
+
matches = eval("collection.#{k.to_s}", binding, __FILE__, __LINE__).index(v)
|
135
|
+
rescue
|
136
|
+
raise ValidationError, "collection attribute not found: #{k.to_s}"
|
805
137
|
end
|
806
|
-
dialog = nil
|
807
138
|
end
|
808
|
-
|
139
|
+
collection if matches
|
140
|
+
end.first
|
809
141
|
end
|
810
142
|
end
|
811
|
-
|
812
|
-
@w.text! "\n"
|
813
|
-
if @footer then @w.p { @w.text! @footer } end
|
814
|
-
@w.text! "\n"
|
815
|
-
|
816
|
-
#unless @dialog.nil?
|
817
|
-
if @dialogs
|
818
|
-
@w.h2 { @w.text! 'Recorded client/server dialogs' }
|
819
|
-
@w.text! "\n"
|
820
|
-
@diarefs.each do |k, v|
|
821
|
-
dialog = @dialogs[k]
|
822
|
-
@w.h3(:id => "dia-#{v}") do
|
823
|
-
@w.text! k
|
824
|
-
end
|
825
|
-
@w.div(:class => 'dialog') do
|
826
|
-
|
827
|
-
@w.div(:class => 'dialab') do
|
828
|
-
@w.text! "\nTo server:\n"
|
829
|
-
dialog.grep(/^>/).each { |crumb| show_message(crumb, :to) }
|
830
|
-
end
|
831
|
-
@w.div( :class => 'dialab' ) do
|
832
|
-
@w.text! "\nFrom Server:\n"
|
833
|
-
dialog.grep(/^</).each { |crumb| show_message(crumb, :from) }
|
834
|
-
end
|
835
|
-
end
|
836
|
-
end
|
837
|
-
end
|
838
|
-
end
|
839
|
-
end
|
840
|
-
end
|
841
|
-
|
842
|
-
def report_li(dialog, marker, text)
|
843
|
-
@w.li do
|
844
|
-
@w.p do
|
845
|
-
if marker
|
846
|
-
write_mark marker
|
847
|
-
@w.text! ' '
|
848
|
-
end
|
849
|
-
# preserve line-breaks in output
|
850
|
-
lines = text.split("\n")
|
851
|
-
lines[0 .. -2].each do |line|
|
852
|
-
@w.text! line
|
853
|
-
@w.br
|
854
|
-
end
|
855
|
-
@w.text! lines[-1] if lines[-1]
|
856
|
-
|
857
|
-
if dialog
|
858
|
-
@w.a(:class => 'diaref', :href => "#dia-#{@dianum}") do
|
859
|
-
@w.text! ' [Dialog]'
|
860
|
-
end
|
861
|
-
@diarefs[dialog] = @dianum
|
862
|
-
@dianum += 1
|
863
|
-
end
|
864
|
-
end
|
865
|
-
end
|
866
|
-
@w.text! "\n"
|
867
|
-
end
|
868
|
-
|
869
|
-
def show_message(crumb, tf)
|
870
|
-
message = crumb[1 .. -1]
|
871
|
-
message.gsub!(/^\s*"/, '')
|
872
|
-
message.gsub!(/"\s*$/, '')
|
873
|
-
message.gsub!(/\\"/, '"')
|
874
|
-
message = Escaper.escape message
|
875
|
-
message.gsub!(/\\n/, "\n<br/>")
|
876
|
-
message.gsub!(/\\t/, '    ')
|
877
|
-
@w.div(:class => tf) { @w.target! << message }
|
878
|
-
end
|
879
|
-
|
880
|
-
def report_text(output=STDOUT)
|
881
|
-
output.puts @header if @header
|
882
|
-
@steps.each do |step|
|
883
|
-
if step.class == Crumbs
|
884
|
-
output.puts " Dialog:"
|
885
|
-
step.each { |crumb| output.puts " #{crumb}" }
|
886
|
-
else
|
887
|
-
body = step[1 .. -1]
|
888
|
-
case step[0,1]
|
889
|
-
when "W"
|
890
|
-
line
|
891
|
-
output.puts "WARNING: #{body}"
|
892
|
-
when "E"
|
893
|
-
line
|
894
|
-
output.puts "ERROR: #{body}"
|
895
|
-
when "G"
|
896
|
-
line
|
897
|
-
output.puts body
|
898
|
-
when "L"
|
899
|
-
line
|
900
|
-
output.puts body
|
901
|
-
when "e"
|
902
|
-
# no-op
|
903
|
-
when "I"
|
904
|
-
output.puts " #{body}"
|
905
|
-
when "D"
|
906
|
-
# later, dude
|
907
|
-
else
|
908
|
-
line
|
909
|
-
output.puts "HUH? #{body}"
|
910
|
-
end
|
911
143
|
end
|
912
|
-
output.puts @footer if @footer
|
913
144
|
end
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
problems += 1 if compare1(e1, e2, e1Name, e2Name, field)
|
920
|
-
end
|
921
|
-
return problems == 0
|
922
|
-
end
|
923
|
-
|
924
|
-
def compare1(e1, e2, e1Name, e2Name, field)
|
925
|
-
c1 = e1.child_content(field)
|
926
|
-
c2 = e2.child_content(field)
|
927
|
-
if c1 != c2
|
928
|
-
problem = true
|
929
|
-
if c1 == nil
|
930
|
-
warning "'#{field}' absent in #{e1Name}."
|
931
|
-
elsif c2 == nil
|
932
|
-
warning "'#{field}' absent in #{e2Name}."
|
933
|
-
else
|
934
|
-
t1 = e1.child_type(field)
|
935
|
-
t2 = e2.child_type(field)
|
936
|
-
if t1 != t2
|
937
|
-
warning "'#{field}' has type='#{t1}' " +
|
938
|
-
"in #{e1Name}, type='#{t2}' in #{e2Name}."
|
939
|
-
else
|
940
|
-
c1 = Escaper.escape(c1)
|
941
|
-
c2 = Escaper.escape(c2)
|
942
|
-
warning "'#{field}' in #{e1Name} [#{c1}] " +
|
943
|
-
"differs from that in #{e2Name} [#{c2}]."
|
944
|
-
end
|
945
|
-
end
|
145
|
+
|
146
|
+
#ensure all variables are setted
|
147
|
+
raise ValidationError, "#{manifest.join("\n")} haven't been setted" if variables.empty?
|
148
|
+
variables.each do |k, v|
|
149
|
+
raise ValidationError, "#{k} haven't been setted" unless v
|
946
150
|
end
|
947
|
-
|
151
|
+
|
152
|
+
variables
|
948
153
|
end
|
154
|
+
|
155
|
+
private
|
949
156
|
|
950
|
-
def
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
when :question
|
955
|
-
@w.span(:class => 'warning') { @w.text! '?' }
|
956
|
-
when :exclamation
|
957
|
-
@w.span(:class => 'error') { @w.text! '!' }
|
958
|
-
when :info
|
959
|
-
@w.img(:align => 'top', :src => '../ape/info.png')
|
960
|
-
end
|
157
|
+
def check_validator(option)
|
158
|
+
validator = Validator.instance(option, @reporter, @authent)
|
159
|
+
opts = check_manifest(validator)
|
160
|
+
validator.validate(opts)
|
961
161
|
end
|
962
162
|
|
963
|
-
def
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
return
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
good("Entry deletion reported success.", name)
|
975
|
-
else
|
976
|
-
error("Couldn't delete the entry: " + deleter.last_error, name)
|
977
|
-
end
|
978
|
-
return worked
|
163
|
+
def get_collection(collections, requested)
|
164
|
+
if (requested.instance_of?(Integer))
|
165
|
+
return collections[requested]
|
166
|
+
elsif (requested.to_sym == :first)
|
167
|
+
return collections.first
|
168
|
+
elsif (requested.to_sym == :last)
|
169
|
+
return collections.last
|
170
|
+
end
|
171
|
+
collections.select do |coll|
|
172
|
+
coll if (coll.title == requested)
|
173
|
+
end.first
|
979
174
|
end
|
980
175
|
end
|
981
176
|
end
|
982
|
-
|