docbook_files 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -4
- data/Gemfile.lock +9 -4
- data/History.txt +10 -0
- data/LICENSE +22 -0
- data/README.md +30 -18
- data/Rakefile +15 -1
- data/lib/docbook_files/app.rb +112 -79
- data/lib/docbook_files/docbook.rb +2 -2
- data/lib/docbook_files/file_data.rb +125 -83
- data/lib/docbook_files/file_ref.rb +98 -0
- data/lib/docbook_files/file_ref_types.rb +20 -0
- data/spec/docbook_files/app_spec.rb +69 -15
- data/spec/docbook_files/docbook_spec.rb +1 -1
- data/spec/docbook_files/file_data_spec.rb +59 -49
- data/spec/docbook_files/file_ref_spec.rb +97 -0
- data/version.txt +1 -1
- metadata +49 -16
@@ -108,7 +108,7 @@ private
|
|
108
108
|
refs = doc.find('//db:*[@fileref!=""]',:db => DOCBOOK_NS)
|
109
109
|
refs.map {|r|
|
110
110
|
fname = r.attributes['fileref']
|
111
|
-
|
111
|
+
FileRef.new(fname,parent_dir,parent_fd)
|
112
112
|
}
|
113
113
|
end
|
114
114
|
|
@@ -116,7 +116,7 @@ private
|
|
116
116
|
# Returns a FileData object with its include-tree
|
117
117
|
#
|
118
118
|
def analyze_file(fname, parent_dir, parent_fd=nil)
|
119
|
-
fl =
|
119
|
+
fl = FileRef.new(fname, parent_dir, parent_fd)
|
120
120
|
if fl.exists?
|
121
121
|
begin
|
122
122
|
doc = XML::Document.file(fl.full_name)
|
@@ -4,61 +4,78 @@ module DocbookFiles
|
|
4
4
|
require 'digest/sha1'
|
5
5
|
require 'wand'
|
6
6
|
|
7
|
-
#
|
7
|
+
# Represents the actual file that is included or referenced in a DocBook project.
|
8
|
+
# For every file there should only one FileData instance, so we use a factory method
|
9
|
+
# #FileData.for to create new instances.
|
10
|
+
#
|
8
11
|
class FileData
|
9
12
|
|
10
|
-
# Type for the main/master file
|
11
|
-
TYPE_MAIN = :main
|
12
|
-
# Type for referenced files
|
13
|
-
TYPE_REFERENCE = :ref
|
14
|
-
# Type for included files
|
15
|
-
TYPE_INCLUDE = :inc
|
16
|
-
|
17
13
|
# File exists and no error happened
|
18
14
|
STATUS_OK = 0
|
19
15
|
# File does not exist
|
20
16
|
STATUS_NOT_FOUND = 1
|
21
17
|
# Error while processing the file, see #error_string
|
22
18
|
STATUS_ERR = 2
|
19
|
+
|
20
|
+
# A storage for all FileData instances.
|
21
|
+
@@Files = {}
|
22
|
+
|
23
|
+
# The directory of the main file. All #path names are relative to that
|
24
|
+
@@MainDir = ""
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
+
# Return the FileData storage -- for testing only
|
27
|
+
def self.storage; @@Files; end
|
28
|
+
|
29
|
+
# Return all existing FileData instances
|
30
|
+
def self.files; @@Files.values; end
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
:checksum => "file checksum",
|
32
|
-
:mime => "the file's MIME type",
|
33
|
-
:namespace => "XML namespace, if applicable",
|
34
|
-
:docbook => "DocBook type flag",
|
35
|
-
:version => "DocBook version number, if applicable",
|
36
|
-
:tag => "XML start tag, if applicable",
|
37
|
-
:parent => "parent file, if included or referenced"}
|
38
|
-
x.each { |s,ex|
|
39
|
-
attr_accessor s
|
40
|
-
}
|
41
|
-
x
|
32
|
+
# Reset the FileData storage -- must be done before every run!
|
33
|
+
def self.reset
|
34
|
+
@@Files={}
|
35
|
+
@@MainDir = ""
|
42
36
|
end
|
43
|
-
|
44
|
-
@@vars = init_vars()
|
45
37
|
|
38
|
+
# Factory method for FileData instances. Checks if there is already
|
39
|
+
# an instance.
|
40
|
+
def self.for(name,parent_dir=".")
|
41
|
+
full_name = get_full_name(name, parent_dir)
|
42
|
+
# Initialize the main dir name for path construction
|
43
|
+
@@MainDir = File.dirname(full_name) if @@Files.size == 0
|
44
|
+
key = full_name
|
45
|
+
if (@@Files[key].nil?)
|
46
|
+
@@Files[key] = FileData.new(name, full_name, key, parent_dir)
|
47
|
+
end
|
48
|
+
@@Files[key]
|
49
|
+
end
|
50
|
+
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
attr_accessor :name, :path, :exists
|
53
|
+
attr_accessor :status, :error_string
|
54
|
+
attr_accessor :full_name
|
55
|
+
# Unique key (path+checksum)
|
56
|
+
attr_reader :key
|
57
|
+
# SHA1 checksum, file size in bytes, mime type, last modified timestamp
|
58
|
+
attr_reader :checksum, :size, :mime, :ts
|
59
|
+
# XML data: namespace, docbook flag, namespace version, start tag
|
60
|
+
attr_accessor :namespace, :docbook, :version, :tag
|
61
|
+
|
62
|
+
|
63
|
+
def initialize(name, full_name, key, parent_dir=".")
|
64
|
+
@path = relative2main(full_name)
|
65
|
+
@full_name = full_name
|
66
|
+
@name = File.basename(name)
|
67
|
+
@key = key
|
51
68
|
@namespace = ""
|
52
69
|
@docbook = false
|
53
70
|
@version = ""
|
54
71
|
@tag = ""
|
55
72
|
@error_string = nil
|
56
|
-
@
|
73
|
+
@rels = []
|
57
74
|
if (File.exists?(@full_name))
|
58
75
|
@status = STATUS_OK
|
59
76
|
@ts = File.mtime(full_name)
|
60
77
|
@size = File.size(full_name)
|
61
|
-
@checksum = calc_checksum()
|
78
|
+
@checksum = calc_checksum(full_name)
|
62
79
|
@mime = get_mime_type()
|
63
80
|
else
|
64
81
|
@status = STATUS_NOT_FOUND
|
@@ -67,9 +84,7 @@ module DocbookFiles
|
|
67
84
|
@checksum = ""
|
68
85
|
@mime = ""
|
69
86
|
@error_string = "file not found"
|
70
|
-
end
|
71
|
-
@includes = []
|
72
|
-
@refs = []
|
87
|
+
end
|
73
88
|
end
|
74
89
|
|
75
90
|
|
@@ -78,59 +93,86 @@ module DocbookFiles
|
|
78
93
|
@status != STATUS_NOT_FOUND
|
79
94
|
end
|
80
95
|
|
81
|
-
#
|
82
|
-
def
|
83
|
-
|
84
|
-
files.flatten.reject{|f| f[:status] != STATUS_NOT_FOUND}.map{|f| f.delete(:status); f}
|
96
|
+
# Add a one-way relationship, type and target, to self
|
97
|
+
def add_rel(type, target)
|
98
|
+
@rels << {:type => type, :target => target}
|
85
99
|
end
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
100
|
+
# Add a two-way relationship between self and a number of targets.
|
101
|
+
def add_rels(type, invtype, targets)
|
102
|
+
targets.each {|t|
|
103
|
+
self.add_rel(type, t)
|
104
|
+
t.add_rel(invtype, self)
|
105
|
+
}
|
90
106
|
end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
#
|
107
|
+
# Get all targets for a relation
|
108
|
+
def get_rel_targets(type)
|
109
|
+
@rels.find_all{|rel| rel[:type] == type}.map{|r| r[:target]}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Add included FileDatas. Establishes a two way relationship between self
|
113
|
+
# and the included files:
|
97
114
|
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
115
|
+
# self -> TYPE_INCLUDE -> target
|
116
|
+
# self <- TYPE_INCLUDED_BY <- target
|
117
|
+
#
|
118
|
+
# TODO: should be 'add_includes'
|
119
|
+
def add_includes(incs)
|
120
|
+
add_rels(FileRefTypes::TYPE_INCLUDE, FileRefTypes::TYPE_INCLUDED_BY, incs)
|
121
|
+
end
|
122
|
+
# Retrieves all included FileDatas
|
123
|
+
def includes
|
124
|
+
get_rel_targets(FileRefTypes::TYPE_INCLUDE)
|
125
|
+
end
|
126
|
+
# Retrieves all FileDatas that include self
|
127
|
+
def included_by
|
128
|
+
get_rel_targets(FileRefTypes::TYPE_INCLUDED_BY)
|
102
129
|
end
|
103
130
|
|
104
|
-
#
|
105
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
131
|
+
# Add referenced FileDatas. Establishes a two way relationship between self
|
132
|
+
# and the referenced files:
|
133
|
+
#
|
134
|
+
# self -> TYPE_REFERENCE -> target
|
135
|
+
# self <- TYPE_REFERENCED_BY <- target
|
136
|
+
#
|
137
|
+
def add_references(incs)
|
138
|
+
add_rels(FileRefTypes::TYPE_REFERENCE, FileRefTypes::TYPE_REFERENCED_BY, incs)
|
139
|
+
end
|
140
|
+
# Retrieves all referenced files
|
141
|
+
def references
|
142
|
+
get_rel_targets(FileRefTypes::TYPE_REFERENCE)
|
143
|
+
end
|
144
|
+
# Retrieves all FileDatas that reference self
|
145
|
+
def referenced_by
|
146
|
+
get_rel_targets(FileRefTypes::TYPE_REFERENCED_BY)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Try to find the path of _file_name_ that is relative to directory
|
150
|
+
# of the _main file_.
|
151
|
+
# If there is no common part return the _file_name_.
|
152
|
+
def relative2main(file_name)
|
153
|
+
md = file_name.match("^#{@@MainDir}/")
|
154
|
+
if md.nil?
|
155
|
+
file_name
|
114
156
|
else
|
115
|
-
|
157
|
+
md.post_match
|
116
158
|
end
|
117
159
|
end
|
118
160
|
|
119
|
-
# Return a
|
120
|
-
#
|
121
|
-
#
|
161
|
+
# Return a hash with the values for the passed symbols.
|
162
|
+
#
|
163
|
+
# Example: to_hash([:name, :mime]) would return
|
164
|
+
# {:name => "name", :mime => "application/xml"}.
|
122
165
|
#
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
me2.flatten
|
166
|
+
def to_hash(props)
|
167
|
+
me_hash = {}
|
168
|
+
props.each {|p|
|
169
|
+
if ([:includes, :included_by, :references, :referenced_by].member?(p))
|
170
|
+
me_hash[p] = self.send(p).map{|p2| p2.path}
|
171
|
+
else
|
172
|
+
me_hash[p] = self.send(p)
|
173
|
+
end
|
174
|
+
}
|
175
|
+
me_hash
|
134
176
|
end
|
135
177
|
|
136
178
|
private
|
@@ -140,17 +182,17 @@ private
|
|
140
182
|
#--
|
141
183
|
# Includes hack for Ruby 1.8
|
142
184
|
#++
|
143
|
-
def calc_checksum
|
185
|
+
def calc_checksum(full_name)
|
144
186
|
if RUBY_VERSION=~ /^1.8/
|
145
|
-
contents = open(
|
187
|
+
contents = open(full_name, "rb") {|io| io.read }
|
146
188
|
else
|
147
|
-
contents = IO.binread(
|
189
|
+
contents = IO.binread(full_name)
|
148
190
|
end
|
149
191
|
Digest::SHA1.hexdigest(contents)
|
150
192
|
end
|
151
193
|
|
152
194
|
# Produce the full path for a filename
|
153
|
-
def get_full_name(fname, parent_dir)
|
195
|
+
def self.get_full_name(fname, parent_dir)
|
154
196
|
dir = File.dirname(fname)
|
155
197
|
file = File.basename(fname)
|
156
198
|
full_name = File.expand_path(file,dir)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- encoding:utf-8 -*-
|
2
|
+
module DocbookFiles
|
3
|
+
|
4
|
+
# A FileRef represents a file inclusion (<xi:include href='...') or
|
5
|
+
# reference (<imagedata filref=='...') in DocBook. It points to a FileData
|
6
|
+
# instance, that represents the actual file. Multiple FileRefs can point to
|
7
|
+
# the same FileData.
|
8
|
+
#
|
9
|
+
# A FileRef contains
|
10
|
+
# * the parent FileRef
|
11
|
+
# * the kind of relation (included or referenced)
|
12
|
+
# * the FileData instance
|
13
|
+
#
|
14
|
+
class FileRef
|
15
|
+
|
16
|
+
# TODO file and line?
|
17
|
+
attr_accessor :rel_type, :file_data, :parent
|
18
|
+
attr_reader :includes, :refs
|
19
|
+
|
20
|
+
def initialize(name,parent_dir=".",parent_file=nil)
|
21
|
+
@file_data = FileData.for(name,parent_dir)
|
22
|
+
@parent = (parent_file.nil? ? nil : parent_file.name)
|
23
|
+
@includes = []
|
24
|
+
@refs = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(name, *args, &block)
|
28
|
+
@file_data.send(name,*args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def includes=(incs)
|
32
|
+
@includes = incs
|
33
|
+
@file_data.add_includes(incs.map{|inc| inc.file_data})
|
34
|
+
end
|
35
|
+
|
36
|
+
def refs=(refs)
|
37
|
+
@refs = refs
|
38
|
+
@file_data.add_references(refs.map{|ref| ref.file_data})
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a hash with the values for the passed symbols.
|
42
|
+
# The type is added.
|
43
|
+
#
|
44
|
+
# Example: to_hash([:name, :mime]) would return
|
45
|
+
# {:type => "main", :name => "name", :mime => "application/xml"}.
|
46
|
+
#
|
47
|
+
def to_hash(props,type)
|
48
|
+
me_hash = {:type => type}
|
49
|
+
props.each {|p| me_hash[p] = self.send(p)}
|
50
|
+
me_hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a tree-like array of maps with the
|
54
|
+
# requested properties (symbols)
|
55
|
+
def traverse(props=[],type=FileRefTypes::TYPE_MAIN)
|
56
|
+
me = self.to_hash(props,type)
|
57
|
+
me2 = [me]
|
58
|
+
unless self.refs.empty?()
|
59
|
+
me2 += self.refs.map {|r| r.to_hash(props,FileRefTypes::TYPE_REFERENCE)}
|
60
|
+
end
|
61
|
+
if self.includes.empty?()
|
62
|
+
me2
|
63
|
+
else
|
64
|
+
me2 + self.includes.map {|i| i.traverse(props,FileRefTypes::TYPE_INCLUDE)}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the names and parent files of non-existing files
|
69
|
+
def find_non_existing_files
|
70
|
+
files = traverse([:name, :status, :parent])
|
71
|
+
files.flatten.reject{|f| f[:status] != FileData::STATUS_NOT_FOUND}.map{|f| f.delete(:status); f}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return a tree-like array with all names
|
75
|
+
def names
|
76
|
+
self.traverse([:name])
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Return a table-like array of maps with the
|
81
|
+
# requested properties (symbols). Each entry gets a level
|
82
|
+
# indicator (:level) to show the tree-level.
|
83
|
+
#
|
84
|
+
def traverse_as_table(props,level=0,type=FileRefTypes::TYPE_MAIN)
|
85
|
+
me = self.to_hash(props,type)
|
86
|
+
me[:level] = level
|
87
|
+
me2 = [me]
|
88
|
+
unless self.refs.empty?()
|
89
|
+
me2 += self.refs.map {|r| x = r.to_hash(props,FileRefTypes::TYPE_REFERENCE); x[:level] = level+1; x}
|
90
|
+
end
|
91
|
+
unless self.includes.empty?()
|
92
|
+
me2 += self.includes.map {|i| i.traverse_as_table(props,level+1,FileRefTypes::TYPE_INCLUDE)}
|
93
|
+
end
|
94
|
+
me2.flatten
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module DocbookFiles
|
4
|
+
|
5
|
+
# Contains thre reference type for relations between files in DocBook.
|
6
|
+
class FileRefTypes
|
7
|
+
|
8
|
+
# Type for the main/master file
|
9
|
+
TYPE_MAIN = :main
|
10
|
+
|
11
|
+
# Type for referenced files (via fileref attribute)
|
12
|
+
TYPE_REFERENCE = :ref
|
13
|
+
TYPE_REFERENCED_BY = :refdby
|
14
|
+
|
15
|
+
# Type for included files (XInclude)
|
16
|
+
TYPE_INCLUDE = :inc
|
17
|
+
TYPE_INCLUDED_BY = :incdby
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -1,24 +1,17 @@
|
|
1
1
|
# -*- encoding:utf-8 -*-
|
2
2
|
require_relative '../spec_helper'
|
3
|
+
require 'stringio'
|
3
4
|
|
4
5
|
module DocbookFiles
|
5
6
|
describe App do
|
6
7
|
|
7
8
|
describe "displays file names" do
|
9
|
+
|
8
10
|
it "according to level" do
|
9
11
|
a = App.new
|
10
|
-
|
11
|
-
|
12
|
-
a.format_name(
|
13
|
-
a.format_name(2,main_d+'chapter.xml',main_n).should == ' chapter.xml'
|
14
|
-
a.format_name(3,main_d+'dir4/chapter.xml',main_n).should == ' dir4/chapter.xml'
|
15
|
-
end
|
16
|
-
|
17
|
-
it "in full when not below main file" do
|
18
|
-
a = App.new
|
19
|
-
main_n = '/dir1/dir2/dir3/book.xml'
|
20
|
-
main_d = '/dir1/dir2/dir3/'
|
21
|
-
a.format_name(2,'/dir1/dir2/chapter4.xml',main_n).should == ' /dir1/dir2/chapter4.xml'
|
12
|
+
a.format_name(0,'book.xml').should == 'book.xml'
|
13
|
+
a.format_name(2,'chapter.xml').should == ' chapter.xml'
|
14
|
+
a.format_name(3,'chapter.xml').should == ' chapter.xml'
|
22
15
|
end
|
23
16
|
|
24
17
|
it "shortened when too long" do
|
@@ -26,10 +19,11 @@ module DocbookFiles
|
|
26
19
|
main_d = '/dir1/dir2'*5
|
27
20
|
main_d2 = '/dir0/dir1/dir2'*5
|
28
21
|
main_n = main_d+'/book.xml'
|
29
|
-
a.format_name(2,main_d+'/chapter.xml'
|
22
|
+
a.format_name(2,main_d+'/chapter.xml').should ==
|
23
|
+
' ...r2/dir1/dir2/dir1/dir2/dir1/dir2/dir1/dir2/chapter.xml'
|
30
24
|
expected = " ...r0/dir1/dir2/dir0/dir1/dir2/dir0/dir1/dir2/chapter.xml"
|
31
|
-
a.format_name(2,main_d2+'/chapter.xml'
|
32
|
-
a.format_name(2,main_d2+'/chapter.xml'
|
25
|
+
a.format_name(2,main_d2+'/chapter.xml').should == expected
|
26
|
+
a.format_name(2,main_d2+'/chapter.xml').length.should == 61
|
33
27
|
end
|
34
28
|
|
35
29
|
end
|
@@ -47,5 +41,65 @@ module DocbookFiles
|
|
47
41
|
a.format_size(1024 + App::TB).should == "1TB"
|
48
42
|
a.format_size(1024 + App::PB).should == "XXL"
|
49
43
|
end
|
44
|
+
|
45
|
+
it "can use YAML as output format" do
|
46
|
+
stdout1 = StringIO.new
|
47
|
+
stderr1 = StringIO.new
|
48
|
+
a = App.new({:stdout => stdout1, :stderr => stderr1})
|
49
|
+
a.run(['--outputformat=yaml','spec/fixtures/bookxi.xml'])
|
50
|
+
stderr1.string.should == ""
|
51
|
+
stdout1.string.should_not == ""
|
52
|
+
yml = YAML.load(stdout1.string)
|
53
|
+
yml.size.should == 2
|
54
|
+
hier = yml[:hierarchy]
|
55
|
+
det = yml[:details]
|
56
|
+
hier.size.should == 5
|
57
|
+
hier[0][:type].should == :main
|
58
|
+
hier[0][:path].should == "bookxi.xml"
|
59
|
+
hier[0][:level].should == 0
|
60
|
+
hier[4][:type].should == :inc
|
61
|
+
hier[4][:path].should == "c4/chapter4xi.xml"
|
62
|
+
hier[4][:level].should == 1
|
63
|
+
det.size.should == 5
|
64
|
+
det[0][:path].should == "bookxi.xml"
|
65
|
+
det[0][:error_string].should be_nil
|
66
|
+
det[0][:includes].should == ["chapter2xi.xml", "chapter3xi.xml", "c4/chapter4xi.xml"]
|
67
|
+
det[0][:included_by].should be_empty
|
68
|
+
det[4][:path].should == "c4/chapter4xi.xml"
|
69
|
+
det[4][:error_string].should be_nil
|
70
|
+
det[4][:includes].should be_empty
|
71
|
+
det[4][:included_by].should == ["bookxi.xml"]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "can use JSON as output format" do
|
75
|
+
stdout1 = StringIO.new
|
76
|
+
stderr1 = StringIO.new
|
77
|
+
require 'json'
|
78
|
+
a = App.new({:stdout => stdout1, :stderr => stderr1, :json_available => true})
|
79
|
+
a.run(['--outputformat=json','spec/fixtures/bookxi.xml'])
|
80
|
+
stderr1.string.should == ""
|
81
|
+
stdout1.string.should_not == ""
|
82
|
+
yml = JSON.parse(stdout1.string)
|
83
|
+
yml.size.should == 2
|
84
|
+
hier = yml["hierarchy"]
|
85
|
+
det = yml["details"]
|
86
|
+
hier.size.should == 5
|
87
|
+
hier[0]["type"].should == "main"
|
88
|
+
hier[0]["path"].should == "bookxi.xml"
|
89
|
+
hier[0]["level"].should == 0
|
90
|
+
hier[4]["type"].should == "inc"
|
91
|
+
hier[4]["path"].should == "c4/chapter4xi.xml"
|
92
|
+
hier[4]["level"].should == 1
|
93
|
+
det.size.should == 5
|
94
|
+
det[0]["path"].should == "bookxi.xml"
|
95
|
+
det[0]["error_string"].should be_nil
|
96
|
+
det[0]["includes"].should == ["chapter2xi.xml", "chapter3xi.xml", "c4/chapter4xi.xml"]
|
97
|
+
det[0]["included_by"].should be_empty
|
98
|
+
det[4]["path"].should == "c4/chapter4xi.xml"
|
99
|
+
det[4]["error_string"].should be_nil
|
100
|
+
det[4]["includes"].should be_empty
|
101
|
+
det[4]["included_by"].should == ["bookxi.xml"]
|
102
|
+
end
|
103
|
+
|
50
104
|
end
|
51
105
|
end
|