[api] fix project status for corner cases
[opensuse:adrians-build-service.git] / src / api / lib / project_status_helper.rb
1 require 'xml'
2 require 'ostruct'
3 require 'digest/md5'
4
5 class LinkInfo
6   attr_accessor :project
7   attr_accessor :package
8   attr_accessor :targetmd5
9 end
10
11 class PackInfo
12   attr_reader :version, :release
13   attr_accessor :devel_project, :devel_package
14   attr_accessor :srcmd5, :verifymd5, :error, :link
15   attr_reader :name, :project, :key
16   attr_accessor :develpack
17
18   def initialize(projname, name)
19     @project = projname
20     @name = name
21     @key = projname + "/" + name
22     @failed = Hash.new
23     @last_success = Hash.new
24     @devel_project = nil
25     @devel_package = nil
26     @version = nil
27     @release = nil
28     # we avoid going back in versions by avoiding going back in time
29     # the last built version wins (repos may have different versions)
30     @versiontime = nil
31     @link = LinkInfo.new
32   end
33
34   def set_version(version, release, time)
35     return if @versiontime and @versiontime > time
36     @versiontime = time
37     @version = version
38     @release = release
39   end
40
41   def success(reponame, time, md5)
42     # try to remember last success
43     if @last_success.has_key? reponame
44       return if @last_success[reponame][0] > time
45     end
46     @last_success[reponame] = OpenStruct.new :time => time, :md5 => md5
47   end
48
49   def failure(reponame, time, md5)
50     # we only track the first failure time but latest md5 returned
51     if @failed.has_key? reponame
52       time = @failed[reponame].time
53     end
54     @failed[reponame] = OpenStruct.new :time => time, :md5 => md5
55   end
56
57   def fails
58     ret = Hash.new
59     @failed.each do |repo,tuple|
60       ls = begin @last_success[repo].time rescue 0 end
61       if ls < tuple.time
62         ret[repo] = tuple
63       end
64     end
65     return ret
66   end
67
68   def to_xml(options = {}) 
69     # return packages not having sources
70     return if srcmd5.blank?
71     xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
72     opts = { :project => project,
73              :name => name,
74              :version => version,
75              :srcmd5 => srcmd5,
76              :release => release }
77     unless verifymd5.blank? or verifymd5 == srcmd5
78       opts[:verifymd5] = verifymd5
79     end
80     xml.package(opts) do
81       self.fails.each do |repo,tuple|
82         xml.failure(:repo => repo, :time => tuple.time, :srcmd5 => tuple.md5 )
83       end
84       if develpack
85         xml.develpack(:proj => devel_project, :pack => devel_package) do
86           develpack.to_xml(:builder => xml)
87         end
88       end
89       if @error then xml.error(error) end
90       if @link.project
91         xml.link(:project => @link.project, :package => @link.package, :targetmd5 => @link.targetmd5)
92       end
93     end
94   end
95 end
96
97 class ProjectStatusHelper
98
99   def self.get_xml(backend, uri)
100     key = Digest::MD5.hexdigest(uri)
101     d = Rails.cache.fetch(key, :expires_in => 2.hours) do
102       backend.direct_http( URI(uri), :timeout => 1000 )
103     end
104     XML::Parser.string(d).parse
105   end
106
107   def self.check_md5(proj, backend, packages, mypackages)
108     uri = '/getprojpack?project=%s&withsrcmd5=1&ignoredisable=1' % CGI.escape(proj)
109     packages.each do |package|
110       uri += "&package=" + CGI.escape(package.name)
111     end
112     data = get_xml(backend, uri)
113     data.find('/projpack/project/package').each do |p|
114       packname = p.attributes['name']
115       key = proj + "/" + packname
116       next unless mypackages.has_key?(key)
117       mypackages[key].srcmd5 = p.attributes['srcmd5']
118     end if data
119   end
120
121   def self.update_projpack(proj, backend, mypackages)
122     uri = '/getprojpack?project=%s&withsrcmd5=1&ignoredisable=1' % CGI.escape(proj)
123     mypackages.each do |key, package|
124       if package.project == proj
125         uri += "&package=" + CGI.escape(package.name)
126       end
127     end
128
129     data = get_xml(backend, uri)
130     data.find('/projpack/project/package').each do |p|
131       packname = p.attributes['name']
132       key = proj + "/" + packname
133       next unless mypackages.has_key?(key)
134       if p.attributes['verifymd5']
135         mypackages[key].verifymd5 = p.attributes['verifymd5']
136       end
137       mypackages[key].srcmd5 = p.attributes['srcmd5']
138       p.find('linked').each do |l|
139         mypackages[key].link.project = l.attributes['project']
140         mypackages[key].link.package = l.attributes['package']
141         break # the first link will do
142       end
143       p.find('error').each do |e|
144         mypackages[key].error = e.content
145       end
146     end if data
147   end
148
149   def self.fetch_jobhistory(backend, proj, repo, arch, mypackages)
150     # we do some fancy caching in here as the function called is pretty expensive and often called
151     # first we check the last line of the job history (limit 1) and then we check if it changed
152     # against the url we expect to query. As the url is too long to be used as meaningful hash we
153     # generate the md5
154     path = '/build/%s/%s/%s/_jobhistory' % [CGI.escape(proj), CGI.escape(repo), arch]
155     currentlast=backend.direct_http( URI(path + '?limit=1') )
156
157     uri = path + '?code=lastfailures'
158     mypackages.each do |key, package|
159       if package.project == proj
160         uri += "&package=" + CGI.escape(package.name)
161       end
162     end
163
164     key = Digest::MD5.hexdigest(uri)
165
166     lastlast = Rails.cache.read(key + '_last', :raw => true)
167     if currentlast != lastlast 
168       Rails.cache.delete key
169     end
170    
171     Rails.cache.fetch(key, :raw => true) do
172       Rails.cache.write(key + '_last', currentlast, :raw => true)
173       backend.direct_http( URI(uri) , :timeout => 1000 )
174     end
175   end
176
177   def self.update_jobhistory(dbproj, backend, mypackages)
178     dbproj.repositories.each do |r|
179       r.architectures.each do |arch|
180         reponame = r.name + "/" + arch.name
181         d = fetch_jobhistory(backend, dbproj.name, r.name, arch.name, mypackages)
182         data = XML::Parser.string(d).parse
183         if data then
184           data.find('/jobhistlist/jobhist').each do |p|
185             packname = p.attributes['package']
186             key = dbproj.name + "/" + packname
187             next unless mypackages.has_key?(key)
188             code = p.attributes['code']
189             readytime = begin Integer(p['readytime']) rescue 0 end
190             if code == "unchanged" || code == "succeeded"
191               mypackages[key].success(reponame, readytime, p['srcmd5'])
192             else
193               mypackages[key].failure(reponame, readytime, p['srcmd5'])
194             end
195             versrel=p.attributes['versrel'].split('-')
196             mypackages[key].set_version(versrel[0..-2].join('-'), versrel[-1], readytime)
197           end
198         end
199       end
200     end 
201   end
202
203   def self.add_recursively(mypackages, projects, dbpack)
204     name = dbpack.name
205     pack = PackInfo.new(dbpack.db_project.name, name)
206     return if mypackages.has_key? pack.key
207
208     if dbpack.develpackage
209       pack.devel_project = dbpack.develpackage.db_project.name
210       pack.devel_package = dbpack.develpackage.name
211       projects[pack.devel_project] = dbpack.develpackage.db_project
212       add_recursively(mypackages, projects, dbpack.develpackage)
213     end
214     mypackages[pack.key] = pack
215   end
216
217   def self.move_devel_package(mypackages, key)
218     return unless mypackages.has_key? key
219
220     pack = mypackages[key]
221     return unless pack.devel_project
222     
223     newkey = pack.devel_project + "/" + pack.devel_package
224     return unless mypackages.has_key? newkey
225     develpack = mypackages[newkey]
226     pack.develpack = develpack
227     key = develpack.project + "/" + develpack.name
228     # recursion for the devel packages
229     move_devel_package(mypackages, key)
230   end
231
232   def self.filter_by_package_name(name)
233     #return (name =~ /Botan/)
234     return true
235   end
236
237   def self.calc_status(dbproj, backend)
238     mypackages = Hash.new
239
240     if ! dbproj
241       puts "invalid project " + proj
242       return mypackages
243     end
244     projects = Hash.new
245     projects[dbproj.name] = dbproj
246     dbproj.db_packages.each do |dbpack|
247       next unless filter_by_package_name(dbpack.name)
248       begin
249         dbpack.resolve_devel_package
250       rescue DbPackage::CycleError => e
251         next
252       end
253       add_recursively(mypackages, projects, dbpack)
254     end
255
256     projects.each do |name,proj|
257       update_jobhistory(proj, backend, mypackages)
258       update_projpack(name, backend, mypackages)
259     end
260
261     dbproj.db_packages.each do |dbpack|
262       next unless filter_by_package_name(dbpack.name)
263       key = dbproj.name + "/" + dbpack.name
264       move_devel_package(mypackages, key)
265     end
266
267     links = Hash.new
268     # find links
269     mypackages.values.each do |package|
270       if package.project == dbproj.name and package.link.project
271         links[package.link.project] ||= Array.new
272         links[package.link.project] << package.link.package
273       end
274     end
275     links.each do |proj, packages|
276       tocheck = Array.new
277       packages.each do |name|
278         pack = PackInfo.new(proj, name)
279         next if mypackages.has_key? pack.key
280         tocheck << pack
281         mypackages[pack.key] = pack
282       end
283       check_md5(proj, backend, tocheck, mypackages) unless tocheck.empty?
284     end
285     
286     mypackages.values.each do |package|
287       if package.project == dbproj.name and package.link.project
288         newkey = package.link.project + "/" + package.link.package
289         package.link.targetmd5 = mypackages[newkey].srcmd5
290       end
291     end
292
293     # cleanup
294     mypackages.keys.each do |key|
295       mypackages.delete(key) if mypackages[key].project != dbproj.name
296     end
297     
298     return mypackages
299   end
300
301   def self.logger
302     RAILS_DEFAULT_LOGGER
303   end
304   
305 end
306