active_bugzilla 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2013-2014 Red Hat, Inc.
2
+
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # ActiveBugzilla
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/active_bugzilla.png)](http://badge.fury.io/rb/active_bugzilla)
4
+ [![Build Status](https://travis-ci.org/ManageIQ/active_bugzilla.png)](https://travis-ci.org/ManageIQ/active_bugzilla)
5
+ [![Code Climate](https://codeclimate.com/github/ManageIQ/active_bugzilla.png)](https://codeclimate.com/github/ManageIQ/active_bugzilla)
6
+ [![Coverage Status](https://coveralls.io/repos/ManageIQ/active_bugzilla/badge.png?branch=master)](https://coveralls.io/r/ManageIQ/active_bugzilla)
7
+ [![Dependency Status](https://gemnasium.com/ManageIQ/active_bugzilla.png)](https://gemnasium.com/ManageIQ/active_bugzilla)
8
+
9
+ ActiveBugzilla is an ActiveRecord like interface to the Bugzilla API.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'active_bugzilla'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install active_bugzilla
24
+
25
+ ## Example Usage
26
+
27
+ ```ruby
28
+ service = ActiveBugzilla::Service.new("http://uri.to/bugzilla", username, password)
29
+ ActiveBugzilla::Base.service = service
30
+ bugs = ActiveBugzilla::Bug.find(:product => product_name, :status => "NEW")
31
+ bugs.each do |bug|
32
+ puts "Bug ##{bug.id} - created_on=#{bug.created_on}, updated_on=#{bug.updated_on}, priority=#{bug.priority}"
33
+ puts "Bug Attributes: #{bug.attribute_names.inspect}"
34
+ end
35
+
36
+ bug = ActiveBugzilla::Bug.find(:id => 12345)
37
+ puts "PRIORITY: #{bug.priority}" # => "low"
38
+ puts "FLAGS: #{bug.flags.inspect}" # => {"devel_ack"=>"?", "qa_ack"=>"+"}
39
+ bug.priority = "high"
40
+ bug.flags.delete("qa_ack")
41
+ bug.flags["devel_ack"] = "+"
42
+ bug.save
43
+ puts "PRIORITY: #{bug.priority}" # => "high"
44
+ puts "FLAGS: #{bug.flags.inspect}" # => {"devel_ack"=>"+"}
45
+ puts "FLAG OBJECTS: #{bug.flag_objects.inspect}" # => Array of ActiveBugzilla:Flag objects
46
+
47
+ bug.add_comment("Testing")
48
+ puts "COMMENTS: #{bug.comments.inspect}" # => Array of ActiveBugzilla:Comment objects
49
+ ```
50
+
51
+ ## Contributing
52
+
53
+ 1. Fork it
54
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
55
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
56
+ 4. Push to the branch (`git push origin my-new-feature`)
57
+ 5. Create new Pull Request
@@ -0,0 +1,12 @@
1
+ require 'active_bugzilla/version'
2
+
3
+ require 'active_bugzilla/service'
4
+
5
+ require 'active_bugzilla/base'
6
+ require 'active_bugzilla/bug'
7
+ require 'active_bugzilla/comment'
8
+ require 'active_bugzilla/field'
9
+ require 'active_bugzilla/flag'
10
+
11
+ module ActiveBugzilla
12
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveBugzilla
2
+ class Base
3
+ def self.service=(service)
4
+ @@service = service
5
+ end
6
+
7
+ def self.service
8
+ @@service
9
+ end
10
+
11
+ private
12
+
13
+ def self.normalize_timestamp(timestamp)
14
+ timestamp.respond_to?(:to_time) ? timestamp.to_time : nil
15
+ end
16
+
17
+ def normalize_timestamp(timestamp)
18
+ self.class.normalize_timestamp(timestamp)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,107 @@
1
+ require 'active_model'
2
+
3
+ module ActiveBugzilla
4
+ class Bug < Base
5
+ include ActiveModel::Validations
6
+ include ActiveModel::Dirty
7
+
8
+ require_relative 'bug/service_management'
9
+ include ServiceManagement
10
+
11
+ require_relative 'bug/flags_management'
12
+ include FlagsManagement
13
+
14
+ validates_numericality_of :id
15
+
16
+ def initialize(attributes = {})
17
+ attributes.each do |key, value|
18
+ next unless attribute_names.include?(key)
19
+ ivar_key = "@#{key}"
20
+ instance_variable_set(ivar_key, value)
21
+ end if attributes
22
+ end
23
+
24
+ def save
25
+ return if changes.empty?
26
+ update_attributes(changed_attribute_hash)
27
+ @changed_attributes.clear
28
+ reload
29
+ end
30
+
31
+ def reload
32
+ raw_reset
33
+ reset_instance_variables
34
+ reset_flags
35
+ @comments = Comment.instantiate_from_raw_data(raw_comments)
36
+ self
37
+ end
38
+
39
+ def update_attributes(attributes)
40
+ attributes.delete(:id)
41
+
42
+ attributes.each do |name, value|
43
+ symbolized_name = name.to_sym
44
+ raise "Unknown Attribute #{name}" unless attribute_names.include?(symbolized_name)
45
+ public_send("#{name}=", value)
46
+ if symbolized_name == :flags
47
+ attributes[name] = flags_raw_updates
48
+ else
49
+ @changed_attributes.delete(symbolized_name)
50
+ end
51
+ end
52
+
53
+ raw_update(attributes) unless attributes.empty?
54
+ end
55
+
56
+ def update_attribute(key, value)
57
+ update_attributes(key => value)
58
+ end
59
+
60
+ def comments
61
+ @comments ||= Comment.instantiate_from_raw_data(raw_comments)
62
+ end
63
+
64
+ def add_comment(comment, is_private = false)
65
+ _comment_id = service.add_comment(@id, comment, :is_private => is_private)
66
+ reload
67
+ end
68
+
69
+ def self.fields
70
+ @fields ||= Field.instantiate_from_raw_data(fetch_fields)
71
+ end
72
+
73
+ def self.find(options = {})
74
+ options[:include_fields] ||= []
75
+ options[:include_fields] << :id unless options[:include_fields].include?(:id)
76
+
77
+ fields_to_include = options[:include_fields].dup
78
+
79
+ search(options).collect do |bug_hash|
80
+ fields_to_include.each do |field|
81
+ bug_hash[field] = nil unless bug_hash.key?(field)
82
+ bug_hash[field] = flags_from_raw_flags_data(bug_hash[field]) if field == :flags
83
+ end
84
+ Bug.new(bug_hash)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def reset_instance_variables
91
+ attribute_names do |name|
92
+ next if name == :id
93
+ ivar_name = "@#{name}"
94
+ instance_variable_set(ivar_name, raw_attribute(name))
95
+ end
96
+ end
97
+
98
+ def changed_attribute_hash
99
+ hash = {}
100
+ changes.each do |key, values|
101
+ _value_from, value_to = values
102
+ hash[key.to_sym] = value_to
103
+ end
104
+ hash
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,88 @@
1
+ require 'active_support/concern'
2
+ require 'dirty_hashy'
3
+
4
+ module ActiveBugzilla::Bug::FlagsManagement
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def flags_from_raw_flags_data(raw_flags_data)
9
+ return {} if raw_flags_data.nil?
10
+ flag_objects = ActiveBugzilla::Flag.instantiate_from_raw_data(raw_flags_data)
11
+ flag_objects.each_with_object({}) do |flag, hash|
12
+ hash[flag.name] = flag.status
13
+ end
14
+ end
15
+ end
16
+
17
+ def flag_objects
18
+ @flag_objects ||= ActiveBugzilla::Flag.instantiate_from_raw_data(raw_flags, @id)
19
+ end
20
+
21
+ def flags=(value)
22
+ flags_will_change! unless value == @flags
23
+ @flags = value
24
+ end
25
+
26
+ def flags
27
+ @flags ||= begin
28
+ flags_hash = flag_objects.each_with_object(DirtyIndifferentHashy.new) do |flag, hash|
29
+ hash[flag.name] = flag.status
30
+ end
31
+ flags_hash.clean_up!
32
+ flags_hash
33
+ end
34
+ end
35
+
36
+ def flags_raw_updates
37
+ raw_updates = []
38
+ flags.changes.each do |key, value|
39
+ _old_status, new_status = value
40
+ new_status ||= 'X'
41
+ raw_updates << {'name' => key.to_s, "status" => new_status}
42
+ end
43
+ raw_updates
44
+ end
45
+
46
+ def reset_flags
47
+ @flag_objects = nil
48
+ @flags = nil
49
+ flags
50
+ end
51
+
52
+ def changed_with_flags?
53
+ changed_without_flags? || flags.changed?
54
+ end
55
+
56
+ def changes_with_flags
57
+ changes = changes_without_flags
58
+ changes['flags'] = [flags_previous_value, flags] if flags.changed?
59
+ changes
60
+ end
61
+
62
+ def flags_previous_value
63
+ previous_flags = flags.dup
64
+ flags.changes.each do |key, value|
65
+ previous_flags[key] = value.first
66
+ end
67
+ previous_flags
68
+ end
69
+
70
+ def changed_attributes_with_flags
71
+ changed_attributes = changed_attributes_without_flags
72
+ changed_attributes['flags'] = flags_previous_value if flags.changed?
73
+ changed_attributes
74
+ end
75
+
76
+ included do
77
+ define_attribute_methods [:flags]
78
+
79
+ alias_method :changed_without_flags?, :changed?
80
+ alias_method :changed?, :changed_with_flags?
81
+
82
+ alias_method :changes_without_flags, :changes
83
+ alias_method :changes, :changes_with_flags
84
+
85
+ alias_method :changed_attributes_without_flags, :changed_attributes
86
+ alias_method :changed_attributes, :changed_attributes_with_flags
87
+ end
88
+ end
@@ -0,0 +1,174 @@
1
+ require 'active_support/concern'
2
+ module ActiveBugzilla::Bug::ServiceManagement
3
+ extend ActiveSupport::Concern
4
+
5
+ ATTRIBUTES_XMLRPC_RENAMES_MAP = {
6
+ # Bug => XMLRPC
7
+ :created_by => :creator,
8
+ :created_on => :creation_time,
9
+ :duplicate_id => :dupe_of,
10
+ :updated_on => :last_change_time,
11
+
12
+ # Some are absent from what Bugzilla.fields() returns
13
+ :actual_time => :actual_time,
14
+ :flags => :flags,
15
+ }
16
+
17
+ module ClassMethods
18
+ def attributes_xmlrpc_map
19
+ @attributes_xmlrpc_map ||= begin
20
+ hash = generate_xmlrpc_map
21
+ define_attributes(hash.keys)
22
+ hash
23
+ end
24
+ end
25
+
26
+ def xmlrpc_timestamps
27
+ @xmlrpc_timestamps ||= fields.select(&:timestamp?).collect { |field| field.name.to_sym }
28
+ end
29
+
30
+ def default_service_attributes
31
+ attributes_xmlrpc_map.values - [:comments]
32
+ end
33
+
34
+ def normalize_attributes_to_service(hash)
35
+ attributes_xmlrpc_map.each do |bug_key, xmlrpc_key|
36
+ bug_key = bug_key.to_sym
37
+ xmlrpc_key = xmlrpc_key.to_sym
38
+ next if bug_key == xmlrpc_key
39
+ hash[xmlrpc_key] = hash.delete(bug_key)
40
+ end
41
+
42
+ hash[:include_fields] = normalize_include_fields_to_service(hash[:include_fields]) if hash.key?(:include_fields)
43
+
44
+ hash.delete_if { |k, v| v.nil? }
45
+ hash
46
+ end
47
+
48
+ def normalize_attributes_from_service(hash)
49
+ attributes_xmlrpc_map.each do |bug_key, xmlrpc_key|
50
+ next unless hash.key?(xmlrpc_key.to_s)
51
+ value = hash.delete(xmlrpc_key.to_s)
52
+ value = normalize_timestamp(value) if xmlrpc_timestamps.include?(xmlrpc_key)
53
+ hash[bug_key] = value
54
+ end
55
+
56
+ hash
57
+ end
58
+
59
+ def attribute_names
60
+ @attribute_names ||= attributes_xmlrpc_map.keys.sort_by { |sym| sym.to_s }
61
+ end
62
+
63
+ def search(options = {})
64
+ options = normalize_attributes_to_service(options)
65
+ service.search(options).collect do |bug_hash|
66
+ normalize_attributes_from_service(bug_hash)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def fetch_fields
73
+ service.fields
74
+ end
75
+
76
+ def generate_xmlrpc_map
77
+ hash = ATTRIBUTES_XMLRPC_RENAMES_MAP
78
+ fields.each do |field|
79
+ next if hash.values.include?(field.name)
80
+ next if field.name.include?(".")
81
+ attribute_name = field.name
82
+ attribute_name = attribute_name[3..-1] if attribute_name[0..2] == "cf_"
83
+ hash[attribute_name.to_sym] = field.name.to_sym
84
+ end
85
+ hash
86
+ end
87
+
88
+ def normalize_include_fields_to_service(include_fields)
89
+ include_fields.collect do |bug_key|
90
+ attributes_xmlrpc_map[bug_key]
91
+ end.uniq.compact
92
+ end
93
+
94
+ def define_attributes(names)
95
+ define_attribute_methods names
96
+
97
+ names.each do |name|
98
+ next if name.to_s == 'flags' # Flags is a special attribute
99
+
100
+ ivar_name = "@#{name}"
101
+ define_method(name) do
102
+ return instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
103
+ instance_variable_set(ivar_name, raw_attribute(name))
104
+ end
105
+
106
+ define_method("#{name}=") do |val|
107
+ public_send("#{name}_will_change!") unless val == instance_variable_get(ivar_name)
108
+ instance_variable_set(ivar_name, val)
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def attribute_names
115
+ self.class.attribute_names
116
+ end
117
+
118
+ private
119
+
120
+ def service
121
+ self.class.service
122
+ end
123
+
124
+ def raw_reset
125
+ @raw_data = nil
126
+ @raw_comments = nil
127
+ @raw_flags = nil
128
+ @raw_attributes = nil
129
+ end
130
+
131
+ def raw_update(attributes)
132
+ attributes = self.class.normalize_attributes_to_service(attributes)
133
+ result = service.update(@id, attributes).first
134
+
135
+ id = result['id']
136
+ raise "Error - Expected to update id <#{@id}>, but updated <#{id}>" unless id == @id
137
+
138
+ result
139
+ end
140
+
141
+ def raw_data
142
+ @raw_data ||= service.get(@id, :include_fields => self.class.default_service_attributes).first
143
+ end
144
+
145
+ def raw_flags
146
+ @raw_flags ||= raw_attribute('flags')
147
+ end
148
+
149
+ def raw_comments
150
+ @raw_comments ||= (raw_attributes['comments'] || fetch_comments)
151
+ end
152
+
153
+ def raw_attributes
154
+ @raw_attributes ||= self.class.normalize_attributes_from_service(raw_data)
155
+ end
156
+
157
+ def raw_attribute_set(key, value)
158
+ raw_attributes
159
+ @raw_attributes[key] = value
160
+ end
161
+
162
+ def raw_attribute(key)
163
+ raw_attribute_set(key, fetch_attribute(key)) unless raw_attributes.key?(key)
164
+ raw_attributes[key]
165
+ end
166
+
167
+ def fetch_comments
168
+ service.comments(:ids => @id)['bugs'][@id.to_s]['comments']
169
+ end
170
+
171
+ def fetch_attribute(key)
172
+ service.get(@id, :include_fields => [key]).first[key]
173
+ end
174
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveBugzilla
2
+ class Comment < Base
3
+ attr_reader :bug_id, :count, :created_by, :created_on, :creator_id, :id, :private, :text, :updated_on
4
+ alias_method :private?, :private
5
+
6
+ def initialize(attributes)
7
+ @created_by = attributes['author']
8
+ @bug_id = attributes['bug_id']
9
+ @count = attributes['count']
10
+ @creator_id = attributes['creator_id']
11
+ @id = attributes['id']
12
+ @text = attributes['text']
13
+
14
+ @created_on = normalize_timestamp attributes['creation_time']
15
+ @updated_on = normalize_timestamp attributes['time']
16
+ @private = attributes['is_private']
17
+ end
18
+
19
+ def self.instantiate_from_raw_data(data)
20
+ data.sort_by(&:count).collect { |hash| new(hash) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ module ActiveBugzilla
2
+ class Field < Base
3
+ attr_reader :display_name, :id, :name, :original_name, :type, :values, :visibility_field, :visibility_values
4
+ attr_reader :is_custom, :is_mandatory, :is_on_bug_entry
5
+ alias_method :mandatory?, :is_mandatory
6
+ alias_method :custom?, :is_custom
7
+ alias_method :on_bug_entry?, :is_on_bug_entry
8
+
9
+ KNOWN_TIMESTAMPS = %w(creation_time last_change_time)
10
+
11
+ # List of field aliases. Maps old style RHBZ parameter names to actual
12
+ # upstream values. Used for createbug() and query include_fields at
13
+ # least.
14
+ FIELD_ALIASES = {
15
+ # old => current
16
+ 'short_desc' => 'summary',
17
+ 'comment' => 'description',
18
+ 'rep_platform' => 'platform',
19
+ 'bug_severity' => 'severity',
20
+ 'bug_status' => 'status',
21
+ 'bug_id' => 'id',
22
+ 'blockedby' => 'blocks',
23
+ 'blocked' => 'blocks',
24
+ 'dependson' => 'depends_on',
25
+ 'reporter' => 'creator',
26
+ 'bug_file_loc' => 'url',
27
+ 'dupe_id' => 'dupe_of',
28
+ 'dup_id' => 'dupe_of',
29
+ 'longdescs' => 'comments',
30
+ 'opendate' => 'creation_time',
31
+ 'creation_ts' => 'creation_time',
32
+ 'status_whiteboard' => 'whiteboard',
33
+ 'delta_ts' => 'last_change_time',
34
+ }
35
+
36
+ def initialize(attributes = {})
37
+ @display_name = attributes["display_name"]
38
+ @id = attributes["id"]
39
+ @name = self.class.field_alias(attributes["name"])
40
+ @original_name = attributes["name"]
41
+ @type = attributes["type"]
42
+ @values = attributes["values"]
43
+ @visibility_field = attributes["visibility_field"]
44
+ @visibility_values = attributes["visibility_values"]
45
+ @is_custom = attributes["is_custom"]
46
+ @is_mandatory = attributes["is_mandatory"]
47
+ @is_on_bug_entry = attributes["is_on_bug_entry"]
48
+ end
49
+
50
+ def timestamp?
51
+ (type == 5) || KNOWN_TIMESTAMPS.include?(name)
52
+ end
53
+
54
+ def self.instantiate_from_raw_data(data)
55
+ data.delete_if { |hash| hash["name"] == "longdesc" } # Another way to specify comment[0]
56
+ data.delete_if { |hash| hash["name"].include?(".") } # Remove things like longdescs.count
57
+ data.collect do |field_hash|
58
+ new(field_hash)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def self.field_alias(value)
65
+ FIELD_ALIASES[value] || value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveBugzilla
2
+ class Flag < Base
3
+ attr_reader :active, :bug_id, :created_on, :id, :name, :setter, :status, :type_id, :updated_on
4
+ alias_method :active?, :active
5
+
6
+ def initialize(attributes)
7
+ @id = attributes['id']
8
+ @bug_id = attributes['bug_id']
9
+ @type_id = attributes['type_id']
10
+ @created_on = normalize_timestamp(attributes['creation_date'])
11
+ @updated_on = normalize_timestamp(attributes['modification_date'])
12
+ @status = attributes['status']
13
+ @name = attributes['name']
14
+ @setter = attributes['setter']
15
+ @active = attributes['is_active']
16
+ end
17
+
18
+ def self.instantiate_from_raw_data(data, bug_id = nil)
19
+ data.collect { |hash| new(hash.merge('bug_id' => bug_id)) }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,234 @@
1
+ require 'xmlrpc/client'
2
+
3
+ module ActiveBugzilla
4
+ class Service
5
+ CLONE_FIELDS = [
6
+ :assigned_to,
7
+ :cc,
8
+ :cf_devel_whiteboard,
9
+ :cf_internal_whiteboard,
10
+ :comments,
11
+ :component,
12
+ :description,
13
+ :groups,
14
+ :keywords,
15
+ :op_sys,
16
+ :platform,
17
+ :priority,
18
+ :product,
19
+ :qa_contact,
20
+ :severity,
21
+ :summary,
22
+ :target_release,
23
+ :url,
24
+ :version,
25
+ :whiteboard
26
+ ]
27
+
28
+ attr_accessor :bugzilla_uri, :username, :password, :last_command
29
+ attr_reader :bugzilla_request_uri, :bugzilla_request_hostname
30
+
31
+ def self.timeout=(value)
32
+ @@timeout = value
33
+ end
34
+
35
+ def self.timeout
36
+ defined?(@@timeout) && @@timeout
37
+ end
38
+
39
+ def timeout
40
+ self.class.timeout
41
+ end
42
+
43
+ def self.product=(value)
44
+ @@product = value
45
+ end
46
+
47
+ def self.product
48
+ defined?(@@product) && @@product
49
+ end
50
+
51
+ def product
52
+ self.class.product
53
+ end
54
+
55
+ def bugzilla_uri=(value)
56
+ @bugzilla_request_uri = URI.join(value, "xmlrpc.cgi").to_s
57
+ @bugzilla_request_hostname = URI(value).hostname
58
+ @bugzilla_uri = value
59
+ end
60
+
61
+ def initialize(bugzilla_uri, username, password)
62
+ raise ArgumentError, "username and password must be set" if username.nil? || password.nil?
63
+
64
+ self.bugzilla_uri = bugzilla_uri
65
+ self.username = username
66
+ self.password = password
67
+ end
68
+
69
+ def inspect
70
+ super.gsub(/@password=\".+?\", /, "")
71
+ end
72
+
73
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#comments
74
+ def comments(params = {})
75
+ execute('comments', params)
76
+ end
77
+
78
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#add_comment
79
+ def add_comment(bug_id, comment, params = {})
80
+ params[:id] = bug_id
81
+ params[:comment] = comment
82
+ execute('add_comment', params)["id"]
83
+ end
84
+
85
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#fields
86
+ def fields(params = {})
87
+ execute('fields', params)['fields']
88
+ end
89
+
90
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#get
91
+ # XMLRPC Bug Query of an existing bug
92
+ #
93
+ # Example:
94
+ # # Perform an xmlrpc query for a single bug.
95
+ # bz.get(948970)
96
+ #
97
+ # @param bug_id [Array, String, Fixnum] One or more bug ids to process.
98
+ # @return [Array] Array of matching bug hashes.
99
+ def get(bug_ids, params = {})
100
+ bug_ids = Array(bug_ids)
101
+ raise ArgumentError, "bug_ids must be all Numeric" unless bug_ids.all? { |id| id.to_s =~ /^\d+$/ }
102
+
103
+ params[:ids] = bug_ids
104
+
105
+ results = execute('get', params)['bugs']
106
+ return [] if results.nil?
107
+ results
108
+ end
109
+
110
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#search
111
+ def search(params = {})
112
+ params[:creation_time] &&= to_xmlrpc_timestamp(params[:creation_time])
113
+ params[:last_change_time] &&= to_xmlrpc_timestamp(params[:last_change_time])
114
+ params[:product] ||= product if product
115
+
116
+ results = execute('search', params)['bugs']
117
+ return [] if results.nil?
118
+ results
119
+ end
120
+
121
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#update
122
+ def update(ids, params = {})
123
+ params[:ids] = Array(ids)
124
+ execute('update', params)['bugs']
125
+ end
126
+
127
+ # http://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#create
128
+ def create(params)
129
+ execute('create', params)
130
+ end
131
+
132
+ # Clone of an existing bug
133
+ #
134
+ # Example:
135
+ # # Perform a clone of an existing bug, and return the new bug ID.
136
+ # bz.clone(948970)
137
+ #
138
+ # @param bug_id [String, Fixnum] A single bug id to process.
139
+ # @param overrides [Hash] The properties to change from the source bug. Some properties include
140
+ # * <tt>:target_release</tt> - The target release for the new cloned bug.
141
+ # * <tt>:assigned_to</tt> - The person to assign the new cloned bug to.
142
+ # @return [Fixnum] The bug id to the new, cloned, bug.
143
+ def clone(bug_id, overrides = {})
144
+ raise ArgumentError, "bug_id must be numeric" unless bug_id.to_s =~ /^\d+$/
145
+
146
+ existing_bz = get(bug_id, :include_fields => CLONE_FIELDS).first
147
+
148
+ clone_description, clone_comment_is_private = assemble_clone_description(existing_bz)
149
+
150
+ params = {}
151
+ CLONE_FIELDS.each do |field|
152
+ next if field == :comments
153
+ params[field] = existing_bz[field.to_s]
154
+ end
155
+
156
+ # Apply overrides
157
+ overrides.each do |param, value|
158
+ params[param] = value
159
+ end
160
+
161
+ # Apply base clone fields
162
+ params[:cf_clone_of] = bug_id
163
+ params[:description] = clone_description
164
+ params[:comment_is_private] = clone_comment_is_private
165
+
166
+ create(params)[:id.to_s]
167
+ end
168
+
169
+ # Bypass python-bugzilla and use the xmlrpc API directly.
170
+ def execute(action, params)
171
+ cmd = "Bug.#{action}"
172
+
173
+ params[:Bugzilla_login] ||= username
174
+ params[:Bugzilla_password] ||= password
175
+
176
+ self.last_command = command_string(cmd, params)
177
+ xmlrpc_client.call(cmd, params)
178
+ end
179
+
180
+ private
181
+
182
+ DEFAULT_CGI_PATH = '/xmlrpc.cgi'
183
+ DEFAULT_PORT = 443
184
+ DEFAULT_PROXY_HOST = nil
185
+ DEFAULT_PROXY_PORT = nil
186
+ DEFAULT_USE_SSL = true
187
+ DEFAULT_TIMEOUT = 120
188
+
189
+ def xmlrpc_client
190
+ @xmlrpc_client ||= ::XMLRPC::Client.new(
191
+ bugzilla_request_hostname,
192
+ DEFAULT_CGI_PATH,
193
+ DEFAULT_PORT,
194
+ DEFAULT_PROXY_HOST,
195
+ DEFAULT_PROXY_PORT,
196
+ username,
197
+ password,
198
+ DEFAULT_USE_SSL,
199
+ timeout || DEFAULT_TIMEOUT)
200
+ end
201
+
202
+ def to_xmlrpc_timestamp(ts)
203
+ return ts if ts.kind_of?(XMLRPC::DateTime)
204
+ return ts unless ts.respond_to?(:to_time)
205
+ ts = ts.to_time
206
+ XMLRPC::DateTime.new(ts.year, ts.month, ts.day, ts.hour, ts.min, ts.sec)
207
+ end
208
+
209
+ # Build a printable representation of the xmlrcp command executed.
210
+ def command_string(cmd, params)
211
+ clean_params = Hash[params]
212
+ clean_params[:Bugzilla_password] = "********"
213
+ "xmlrpc_client.call(#{cmd}, #{clean_params})"
214
+ end
215
+
216
+ def assemble_clone_description(existing_bz)
217
+ clone_description = " +++ This bug was initially created as a clone of Bug ##{existing_bz[:id]} +++ \n"
218
+ clone_description << existing_bz[:description.to_s]
219
+
220
+ clone_comment_is_private = false
221
+ existing_bz[:comments.to_s].each do |comment|
222
+ clone_description << "\n\n"
223
+ clone_description << "*" * 70
224
+ clone_description << "\nFollowing comment by %s on %s\n\n" %
225
+ [comment['author'], comment['creation_time'].to_time]
226
+ clone_description << "\n\n"
227
+ clone_description << comment['text']
228
+ clone_comment_is_private = true if comment['is_private']
229
+ end
230
+
231
+ [clone_description, clone_comment_is_private]
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveBugzilla
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveBugzilla::Bug do
4
+ context "#new" do
5
+ before(:each) do
6
+ @service_mapping = {
7
+ # Bug => XMLRPC
8
+ :severity => :severity_xmlrpc,
9
+ :priority => :priority_xmlrpc,
10
+ }
11
+ @service = double('service')
12
+ ActiveBugzilla::Base.service = @service
13
+ described_class.stub(:generate_xmlrpc_map).and_return(@service_mapping)
14
+ described_class.stub(:xmlrpc_timestamps).and_return([])
15
+ @id = 123
16
+ @bug = described_class.new(:id => @id)
17
+ end
18
+
19
+ it "attribute_names" do
20
+ raw_keys = @service_mapping.values
21
+ raw_data = {}
22
+ raw_keys.each { |k| raw_data[k.to_s] = 'foo' }
23
+ @bug.stub(:raw_data).and_return(raw_data)
24
+ expect(@bug.attribute_names).to eq(@service_mapping.keys.sort_by { |key| key.to_s })
25
+ end
26
+
27
+ it "severity" do
28
+ severity = 'foo'
29
+ raw_data = {'severity_xmlrpc' => severity}
30
+ @bug.stub(:raw_data).and_return(raw_data)
31
+ expect(@bug.severity).to eq(severity)
32
+ end
33
+
34
+ it "comments" do
35
+ comments_hash = [{'id' => 1}]
36
+ raw_data = {'comments' => comments_hash}
37
+ @bug.stub(:raw_data).and_return(raw_data)
38
+ comments = @bug.comments
39
+ expect(comments).to be_kind_of(Array)
40
+ expect(comments.count).to eq(1)
41
+ expect(comments.first).to be_kind_of(ActiveBugzilla::Comment)
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveBugzilla::Comment do
4
+ before(:each) do
5
+ @author = 'author@example.com'
6
+ @bug_id = 123
7
+ @count = 0
8
+ @id = 42
9
+ @text = "This is a comment"
10
+ @is_private = true
11
+
12
+ @bug_comment = described_class.new(
13
+ 'author' => @author,
14
+ 'bug_id' => @bug_id,
15
+ 'count' => @count,
16
+ 'id' => @id,
17
+ 'text' => @text,
18
+ 'is_private' => @is_private)
19
+ end
20
+
21
+ it "#private?" do
22
+ expect(@bug_comment.private?).to eq(@is_private)
23
+ end
24
+
25
+ it "#created_by" do
26
+ expect(@bug_comment.created_by).to eq(@author)
27
+ end
28
+
29
+ it "#bug_id" do
30
+ expect(@bug_comment.bug_id).to eq(@bug_id)
31
+ end
32
+
33
+ it "#id" do
34
+ expect(@bug_comment.id).to eq(@id)
35
+ end
36
+
37
+ it "#text" do
38
+ expect(@bug_comment.text).to eq(@text)
39
+ end
40
+
41
+ end
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveBugzilla::Service do
4
+ let(:bz) { described_class.new("http://uri.to/bugzilla", "calvin", "hobbes") }
5
+
6
+ context "#new" do
7
+ it 'normal case' do
8
+ expect { bz }.to_not raise_error
9
+ end
10
+
11
+ it "when bugzilla_uri is invalid" do
12
+ expect { described_class.new("lalala", "", "") }.to raise_error(URI::BadURIError)
13
+ end
14
+
15
+ it "when username and password are not set" do
16
+ expect { described_class.new("http://uri.to/bugzilla", nil, nil) }.to raise_error(ArgumentError)
17
+ end
18
+ end
19
+
20
+ context "#get" do
21
+ it "when no argument is specified" do
22
+ expect { bz.get }.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it "when an invalid argument is specified" do
26
+ expect { bz.get("not a Fixnum") }.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it "when the specified bug does not exist" do
30
+ output = {}
31
+
32
+ allow(::XMLRPC::Client).to receive(:new).and_return(double('xmlrpc_client', :call => output))
33
+ matches = bz.get(94897099)
34
+ expect(matches).to be_kind_of(Array)
35
+ expect(matches).to be_empty
36
+ end
37
+
38
+ it "when producing valid output" do
39
+ output = {
40
+ 'bugs' => [
41
+ {
42
+ "priority" => "unspecified",
43
+ "keywords" => ["ZStream"],
44
+ "cc" => ["calvin@redhat.com", "hobbes@RedHat.com"],
45
+ },
46
+ ]
47
+ }
48
+
49
+ allow(::XMLRPC::Client).to receive(:new).and_return(double('xmlrpc_client', :call => output))
50
+ existing_bz = bz.get("948972").first
51
+
52
+ expect(bz.last_command).to include("Bug.get")
53
+
54
+ expect(existing_bz["priority"]).to eq("unspecified")
55
+ expect(existing_bz["keywords"]).to eq(["ZStream"])
56
+ expect(existing_bz["cc"]).to eq(["calvin@redhat.com", "hobbes@RedHat.com"])
57
+ end
58
+ end
59
+
60
+ context "#clone" do
61
+ it "when no argument is specified" do
62
+ expect { bz.clone }.to raise_error(ArgumentError)
63
+ end
64
+
65
+ it "when an invalid argument is specified" do
66
+ expect { bz.clone("not a Fixnum") }.to raise_error(ArgumentError)
67
+ end
68
+
69
+ it "when the specified bug to clone does not exist" do
70
+ output = {}
71
+
72
+ allow(::XMLRPC::Client).to receive(:new).and_return(double('xmlrpc_client', :call => output))
73
+ expect { bz.clone(94897099) }.to raise_error
74
+ end
75
+
76
+ it "when producing valid output" do
77
+ output = {"id" => 948992}
78
+ existing_bz = {
79
+ "description" => "Description of problem:\n\nIt's Broken",
80
+ "priority" => "unspecified",
81
+ "assigned_to" => "calvin@redhat.com",
82
+ "target_release" => ["---"],
83
+ "keywords" => ["ZStream"],
84
+ "cc" => ["calvin@redhat.com", "hobbes@RedHat.com"],
85
+ "comments" => [
86
+ {
87
+ "is_private" => false,
88
+ "count" => 0,
89
+ "time" => XMLRPC::DateTime.new(1969, 7, 20, 16, 18, 30),
90
+ "bug_id" => 948970,
91
+ "author" => "Calvin@redhat.com",
92
+ "text" => "It's Broken and impossible to reproduce",
93
+ "creation_time" => XMLRPC::DateTime.new(1969, 7, 20, 16, 18, 30),
94
+ "id" => 5777871,
95
+ "creator_id" => 349490
96
+ },
97
+ {
98
+ "is_private" => false,
99
+ "count" => 1,
100
+ "time" => XMLRPC::DateTime.new(1970, 11, 10, 16, 18, 30),
101
+ "bug_id" => 948970,
102
+ "author" => "Hobbes@redhat.com",
103
+ "text" => "Fix Me Now!",
104
+ "creation_time" => XMLRPC::DateTime.new(1972, 2, 14, 0, 0, 0),
105
+ "id" => 5782170,
106
+ "creator_id" => 349490
107
+ },
108
+ ]
109
+ }
110
+
111
+ described_class.any_instance.stub(:get).and_return([existing_bz])
112
+ allow(::XMLRPC::Client).to receive(:new).and_return(double('xmlrpc_create', :call => output))
113
+ new_bz_id = bz.clone("948972")
114
+
115
+ expect(bz.last_command).to include("Bug.create")
116
+
117
+ expect(new_bz_id).to eq(output["id"])
118
+ end
119
+
120
+ it "when providing override values" do
121
+ output = {"id" => 948992}
122
+ existing_bz = {
123
+ "description" => "Description of problem:\n\nIt's Broken",
124
+ "priority" => "unspecified",
125
+ "assigned_to" => "calvin@redhat.com",
126
+ "target_release" => ["---"],
127
+ "keywords" => ["ZStream"],
128
+ "cc" => ["calvin@redhat.com", "hobbes@RedHat.com"],
129
+ "comments" => [
130
+ {
131
+ "is_private" => false,
132
+ "count" => 0,
133
+ "time" => XMLRPC::DateTime.new(1969, 7, 20, 16, 18, 30),
134
+ "bug_id" => 948970,
135
+ "author" => "Buzz.Aldrin@redhat.com",
136
+ "text" => "It's Broken and impossible to reproduce",
137
+ "creation_time" => XMLRPC::DateTime.new(1969, 7, 20, 16, 18, 30),
138
+ "id" => 5777871,
139
+ "creator_id" => 349490
140
+ },
141
+ {
142
+ "is_private" => false,
143
+ "count" => 1,
144
+ "time" => XMLRPC::DateTime.new(1970, 11, 10, 16, 18, 30),
145
+ "bug_id" => 948970,
146
+ "author" => "Neil.Armstrong@redhat.com",
147
+ "text" => "Fix Me Now!",
148
+ "creation_time" => XMLRPC::DateTime.new(1972, 2, 14, 0, 0, 0),
149
+ "id" => 5782170,
150
+ "creator_id" => 349490
151
+ },
152
+ ]
153
+ }
154
+
155
+ described_class.any_instance.stub(:get).and_return([existing_bz])
156
+ allow(::XMLRPC::Client).to receive(:new).and_return(double('xmlrpc_create', :call => output))
157
+ new_bz_id = bz.clone("948972", "assigned_to" => "Ham@NASA.gov", "target_release" => ["2.2.0"])
158
+
159
+ expect(bz.last_command).to include("Bug.create")
160
+
161
+ expect(new_bz_id).to eq(output["id"])
162
+ end
163
+ end
164
+
165
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveBugzilla do
4
+ it "::VERSION" do
5
+ described_class::VERSION.should be_kind_of(String)
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+
11
+ # Run specs in random order to surface order dependencies. If you find an
12
+ # order dependency and want to debug it, you can fix the order by providing
13
+ # the seed, which is printed after each run.
14
+ # --seed 1234
15
+ config.order = 'random'
16
+ end
17
+
18
+ begin
19
+ require 'coveralls'
20
+ Coveralls.wear!
21
+ rescue LoadError
22
+ end
23
+
24
+ require 'active_bugzilla'
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_bugzilla
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Joe VLcek
9
+ - Jason Frey
10
+ - Oleg Barenboim
11
+ - Alberto Bellotti
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2014-03-31 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: bundler
19
+ requirement: !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '1.3'
25
+ type: :development
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :development
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ requirement: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ - !ruby/object:Gem::Dependency
66
+ name: coveralls
67
+ requirement: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: activemodel
83
+ requirement: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activesupport
99
+ requirement: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: dirty_hashy
115
+ requirement: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :runtime
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ description: ActiveBugzilla is an ActiveRecord like interface to the Bugzilla API.
130
+ email:
131
+ - jvlcek@redhat.com
132
+ - jfrey@redhat.com
133
+ - chessbyte@gmail.com
134
+ - abellott@redhat.com
135
+ executables: []
136
+ extensions: []
137
+ extra_rdoc_files: []
138
+ files:
139
+ - lib/active_bugzilla.rb
140
+ - lib/active_bugzilla/base.rb
141
+ - lib/active_bugzilla/bug.rb
142
+ - lib/active_bugzilla/bug/flags_management.rb
143
+ - lib/active_bugzilla/bug/service_management.rb
144
+ - lib/active_bugzilla/comment.rb
145
+ - lib/active_bugzilla/field.rb
146
+ - lib/active_bugzilla/flag.rb
147
+ - lib/active_bugzilla/service.rb
148
+ - lib/active_bugzilla/version.rb
149
+ - README.md
150
+ - LICENSE.txt
151
+ - spec/active_bugzilla/bug_spec.rb
152
+ - spec/active_bugzilla/comment_spec.rb
153
+ - spec/active_bugzilla/service_spec.rb
154
+ - spec/active_bugzilla_spec.rb
155
+ - spec/spec_helper.rb
156
+ homepage: http://github.com/ManageIQ/active_bugzilla
157
+ licenses:
158
+ - MIT
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ! '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ none: false
171
+ requirements:
172
+ - - ! '>='
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubyforge_project:
177
+ rubygems_version: 1.8.23
178
+ signing_key:
179
+ specification_version: 3
180
+ summary: ActiveBugzilla is an ActiveRecord like interface to the Bugzilla API.
181
+ test_files:
182
+ - spec/active_bugzilla/bug_spec.rb
183
+ - spec/active_bugzilla/comment_spec.rb
184
+ - spec/active_bugzilla/service_spec.rb
185
+ - spec/active_bugzilla_spec.rb
186
+ - spec/spec_helper.rb
187
+ has_rdoc: