]> git.donarmstrong.com Git - lilypond.git/blob - buildscripts/output-distance.py
output-distance fixes: record git status of source tree in test.
[lilypond.git] / buildscripts / output-distance.py
1 #!@TARGET_PYTHON@
2 import sys
3 import optparse
4 import os
5 import math
6
7 ## so we can call directly as buildscripts/output-distance.py
8 me_path = os.path.abspath (os.path.split (sys.argv[0])[0])
9 sys.path.insert (0, me_path + '/../python/')
10
11
12 import safeeval
13
14
15 X_AXIS = 0
16 Y_AXIS = 1
17 INFTY = 1e6
18
19 OUTPUT_EXPRESSION_PENALTY = 1
20 ORPHAN_GROB_PENALTY = 1
21 options = None
22
23 ################################################################
24 # system interface.
25 temp_dir = None
26 class TempDirectory:
27     def __init__ (self):
28         import tempfile
29         self.dir = tempfile.mkdtemp ()
30         print 'dir is', self.dir
31     def __del__ (self):
32         print 'rm -rf %s' % self.dir 
33         os.system ('rm -rf %s' % self.dir )
34     def __call__ (self):
35         return self.dir
36
37
38 def get_temp_dir  ():
39     global temp_dir
40     if not temp_dir:
41         temp_dir = TempDirectory ()
42     return temp_dir ()
43
44 def read_pipe (c):
45     print 'pipe' , c
46     return os.popen (c).read ()
47
48 def system (c):
49     print 'system' , c
50     s = os.system (c)
51     if s :
52         raise Exception ("failed")
53     return
54
55 def shorten_string (s):
56     threshold = 15 
57     if len (s) > 2*threshold:
58         s = s[:threshold] + '..' + s[-threshold:]
59     return s
60
61 def max_distance (x1, x2):
62     dist = 0.0
63
64     for (p,q) in zip (x1, x2):
65         dist = max (abs (p-q), dist)
66         
67     return dist
68
69
70 def compare_png_images (old, new, dest_dir):
71     def png_dims (f):
72         m = re.search ('([0-9]+) x ([0-9]+)', read_pipe ('file %s' % f))
73         
74         return tuple (map (int, m.groups ()))
75
76     dest = os.path.join (dest_dir, new.replace ('.png', '.compare.jpeg'))
77     try:
78         dims1 = png_dims (old)
79         dims2 = png_dims (new)
80     except AttributeError:
81         ## hmmm. what to do?
82         system ('touch %(dest)s' % locals ())
83         return
84     
85     dims = (min (dims1[0], dims2[0]),
86             min (dims1[1], dims2[1]))
87
88     dir = get_temp_dir ()
89     system ('convert -depth 8 -crop %dx%d+0+0 %s %s/crop1.png' % (dims + (old, dir)))
90     system ('convert -depth 8 -crop %dx%d+0+0 %s %s/crop2.png' % (dims + (new, dir)))
91
92     system ('compare -depth 8 %(dir)s/crop1.png %(dir)s/crop2.png %(dir)s/diff.png' % locals ())
93
94     system ("convert  -depth 8 %(dir)s/diff.png -blur 0x3 -negate -channel alpha,blue -type TrueColorMatte -fx 'intensity'    %(dir)s/matte.png" % locals ())
95
96     system ("composite -quality 65 %(dir)s/matte.png %(new)s %(dest)s" % locals ())
97
98
99 ################################################################
100 # interval/bbox arithmetic.
101
102 empty_interval = (INFTY, -INFTY)
103 empty_bbox = (empty_interval, empty_interval)
104
105 def interval_is_empty (i):
106     return i[0] > i[1]
107
108 def interval_length (i):
109     return max (i[1]-i[0], 0) 
110     
111 def interval_union (i1, i2):
112     return (min (i1[0], i2[0]),
113             max (i1[1], i2[1]))
114
115 def interval_intersect (i1, i2):
116     return (max (i1[0], i2[0]),
117             min (i1[1], i2[1]))
118
119 def bbox_is_empty (b):
120     return (interval_is_empty (b[0])
121             or interval_is_empty (b[1]))
122
123 def bbox_union (b1, b2):
124     return (interval_union (b1[X_AXIS], b2[X_AXIS]),
125             interval_union (b2[Y_AXIS], b2[Y_AXIS]))
126             
127 def bbox_intersection (b1, b2):
128     return (interval_intersect (b1[X_AXIS], b2[X_AXIS]),
129             interval_intersect (b2[Y_AXIS], b2[Y_AXIS]))
130
131 def bbox_area (b):
132     return interval_length (b[X_AXIS]) * interval_length (b[Y_AXIS])
133
134 def bbox_diameter (b):
135     return max (interval_length (b[X_AXIS]),
136                 interval_length (b[Y_AXIS]))
137                 
138
139 def difference_area (a, b):
140     return bbox_area (a) - bbox_area (bbox_intersection (a,b))
141
142 class GrobSignature:
143     def __init__ (self, exp_list):
144         (self.name, self.origin, bbox_x,
145          bbox_y, self.output_expression) = tuple (exp_list)
146         
147         self.bbox = (bbox_x, bbox_y)
148         self.centroid = (bbox_x[0] + bbox_x[1], bbox_y[0] + bbox_y[1])
149
150     def __repr__ (self):
151         return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self.name,
152                                                    self.bbox[0][0],
153                                                    self.bbox[0][1],
154                                                    self.bbox[1][0],
155                                                    self.bbox[1][1])
156                                                  
157     def axis_centroid (self, axis):
158         return apply (sum, self.bbox[axis])  / 2 
159     
160     def centroid_distance (self, other, scale):
161         return max_distance (self.centroid, other.centroid) / scale 
162         
163     def bbox_distance (self, other):
164         divisor = bbox_area (self.bbox) + bbox_area (other.bbox)
165
166         if divisor:
167             return (difference_area (self.bbox, other.bbox) +
168                     difference_area (other.bbox, self.bbox)) / divisor
169         else:
170             return 0.0
171         
172     def expression_distance (self, other):
173         if self.output_expression == other.output_expression:
174             return 0
175         else:
176             return 1
177
178 ################################################################
179 # single System.
180
181 class SystemSignature:
182     def __init__ (self, grob_sigs):
183         d = {}
184         for g in grob_sigs:
185             val = d.setdefault (g.name, [])
186             val += [g]
187
188         self.grob_dict = d
189         self.set_all_bbox (grob_sigs)
190
191     def set_all_bbox (self, grobs):
192         self.bbox = empty_bbox
193         for g in grobs:
194             self.bbox = bbox_union (g.bbox, self.bbox)
195
196     def closest (self, grob_name, centroid):
197         min_d = INFTY
198         min_g = None
199         try:
200             grobs = self.grob_dict[grob_name]
201
202             for g in grobs:
203                 d = max_distance (g.centroid, centroid)
204                 if d < min_d:
205                     min_d = d
206                     min_g = g
207
208
209             return min_g
210
211         except KeyError:
212             return None
213     def grobs (self):
214         return reduce (lambda x,y: x+y, self.grob_dict.values(), [])
215
216 ################################################################
217 ## comparison of systems.
218
219 class SystemLink:
220     def __init__ (self, system1, system2):
221         self.system1 = system1
222         self.system2 = system2
223         
224         self.link_list_dict = {}
225         self.back_link_dict = {}
226
227
228         ## pairs
229         self.orphans = []
230
231         ## pair -> distance
232         self.geo_distances = {}
233
234         ## pairs
235         self.expression_changed = []
236
237         self._geometric_distance = None
238         self._expression_change_count = None
239         self._orphan_count = None
240         
241         for g in system1.grobs ():
242
243             ## skip empty bboxes.
244             if bbox_is_empty (g.bbox):
245                 continue
246             
247             closest = system2.closest (g.name, g.centroid)
248             
249             self.link_list_dict.setdefault (closest, [])
250             self.link_list_dict[closest].append (g)
251             self.back_link_dict[g] = closest
252
253
254     def calc_geometric_distance (self):
255         total = 0.0
256         for (g1,g2) in self.back_link_dict.items ():
257             if g2:
258                 d = g1.bbox_distance (g2)
259                 if d:
260                     self.geo_distances[(g1,g2)] = d
261
262                 total += d
263
264         self._geometric_distance = total
265     
266     def calc_orphan_count (self):
267         count = 0
268         for (g1, g2) in self.back_link_dict.items ():
269             if g2 == None:
270                 self.orphans.append ((g1, None))
271                 
272                 count += 1
273
274         self._orphan_count = count
275     
276     def calc_output_exp_distance (self):
277         d = 0
278         for (g1,g2) in self.back_link_dict.items ():
279             if g2:
280                 d += g1.expression_distance (g2)
281
282         self._expression_change_count = d
283
284     def output_expression_details_string (self):
285         return ', '.join ([g1.name for g1 in self.expression_changed])
286     
287     def geo_details_string (self):
288         results = [(d, g1,g2) for ((g1, g2), d) in self.geo_distances.items()]
289         results.sort ()
290         results.reverse ()
291         
292         return ', '.join (['%s: %f' % (g1.name, d) for (d, g1, g2) in results])
293
294     def orphan_details_string (self):
295         return ', '.join (['%s-None' % g1.name for (g1,g2) in self.orphans if g2==None])
296
297     def geometric_distance (self):
298         if self._geometric_distance == None:
299             self.calc_geometric_distance ()
300         return self._geometric_distance
301     
302     def orphan_count (self):
303         if self._orphan_count == None:
304             self.calc_orphan_count ()
305             
306         return self._orphan_count
307     
308     def output_expression_change_count (self):
309         if self._expression_change_count == None:
310             self.calc_output_exp_distance ()
311         return self._expression_change_count
312         
313     def distance (self):
314         return (self.output_expression_change_count (),
315                 self.orphan_count (),
316                 self.geometric_distance ())
317     
318 def read_signature_file (name):
319     print 'reading', name
320     
321     entries = open (name).read ().split ('\n')
322     def string_to_tup (s):
323         return tuple (map (float, s.split (' '))) 
324
325     def string_to_entry (s):
326         fields = s.split('@')
327         fields[2] = string_to_tup (fields[2])
328         fields[3] = string_to_tup (fields[3])
329
330         return tuple (fields)
331     
332     entries = [string_to_entry (e) for e in entries
333                if e and not e.startswith ('#')]
334
335     grob_sigs = [GrobSignature (e) for e in entries]
336     sig = SystemSignature (grob_sigs)
337     return sig
338
339
340 ################################################################
341 # different systems of a .ly file.
342
343 hash_to_original_name = {}
344
345 class FileLink:
346     def __init__ (self, f1, f2):
347         self._distance = None
348         self.file_names = (f1, f2)
349         
350     def text_record_string (self):
351         return '%-30f %-20s\n' % (self.distance (),
352                                   self.name ())
353     def calc_distance (self):
354         return 0.0
355
356     def distance (self):
357         if self._distance == None:
358            self._distance = self.calc_distance ()
359
360         return self._distance
361     
362         
363     def name (self):
364         base = os.path.basename (self.file_names[1])
365         base = os.path.splitext (base)[0]
366         
367         base = hash_to_original_name.get (base, base)
368         base = os.path.splitext (base)[0]
369         return base
370     
371     def extension (self):
372         return os.path.splitext (self.file_names[1])[1]
373
374     def link_files_for_html (self, dest_dir):
375         for f in self.file_names:
376             link_file (f, os.path.join (dest_dir, f))
377
378     def get_distance_details (self):
379         return ''
380
381     def get_cell (self, oldnew):
382         return ''
383     
384     def get_file (self, oldnew):
385         return self.file_names[oldnew]
386     
387     def html_record_string (self, dest_dir):
388         self.link_files_for_html (dest_dir)
389         
390         dist = self.distance()
391         
392         details = self.get_distance_details ()
393         if details:
394             details_base = os.path.splitext (self.file_names[1])[0]
395             details_base += '.details.html'
396             fn = dest_dir + '/'  + details_base
397             open (fn, 'w').write (details)
398
399             details = '<br>(<a href="%(details_base)s">details</a>)' % locals ()
400
401         cell1 = self.get_cell (0)
402         cell2 = self.get_cell (1)
403
404         name = self.name () + self.extension ()
405         file1 = self.get_file (0)
406         file2 = self.get_file (1)
407         
408         return '''<tr>
409 <td>
410 %(dist)f
411 %(details)s
412 </td>
413 <td>%(cell1)s<br><font size=-2><a href="%(file1)s"><tt>%(name)s</tt></font></td>
414 <td>%(cell2)s<br><font size=-2><a href="%(file2)s"><tt>%(name)s</tt></font></td>
415 </tr>''' % locals ()
416
417
418 class FileCompareLink (FileLink):
419     def __init__ (self, f1, f2):
420         FileLink.__init__ (self, f1, f2)
421         self.contents = (self.get_content (self.file_names[0]),
422                          self.get_content (self.file_names[1]))
423         
424
425     def calc_distance (self):
426         ## todo: could use import MIDI to pinpoint
427         ## what & where changed.
428
429         if self.contents[0] == self.contents[1]:
430             return 0.0
431         else:
432             return 100.0;
433         
434     def get_content (self, f):
435         print 'reading', f
436         s = open (f).read ()
437         return s
438
439
440 class GitFileCompareLink (FileCompareLink):
441     def get_cell (self, oldnew):
442         return self.contents[oldnew]
443     
444     def calc_distance (self):
445         if self.contents[0] == self.contents[1]:
446             d = 0.0
447         else:
448             d = 1.0001 *options.threshold
449
450         print 'dist', d
451         return d
452         
453 class TextFileCompareLink (FileCompareLink):
454     def calc_distance (self):
455         import difflib
456         diff = difflib.unified_diff (self.contents[0].strip().split ('\n'),
457                                      self.contents[1].strip().split ('\n'),
458                                      fromfiledate = self.file_names[0],
459                                      tofiledate = self.file_names[1]
460                                      )
461         
462         self.diff_lines =  [l for l in diff]
463         self.diff_lines = self.diff_lines[2:]
464         
465         return float (len ([l for l in self.diff_lines if l[0] in '-+']))
466         
467     def get_cell (self, oldnew):
468         str = ''
469         if oldnew == 1:
470             str = '\n'.join ([d.replace ('\n','') for d in self.diff_lines])
471         str = '<font size="-2"><pre>%s</pre></font>' % str
472         return str
473
474         
475 class ProfileFileLink (FileCompareLink):
476     def __init__ (self, f1, f2):
477         FileCompareLink.__init__ (self, f1, f2)
478         self.results = [{}, {}]
479     
480     def get_cell (self, oldnew):
481         str = ''
482         for k in ('time', 'cells'):
483             if oldnew==0:
484                 str += '%-8s: %d\n' %  (k, int (self.results[oldnew][k]))
485             else:
486                 str += '%-8s: %8d (%5.3f)\n' % (k, int (self.results[oldnew][k]),
487                                          self.get_ratio (k))
488
489         return '<pre>%s</pre>' % str
490             
491     def get_ratio (self, key):
492         (v1,v2) = (self.results[0].get (key, -1),
493                    self.results[1].get (key, -1))
494
495         if v1 <= 0 or v2 <= 0:
496             return 0.0
497
498         return (v1 - v2) / float (v1+v2)
499     
500     def calc_distance (self):
501         for oldnew in (0,1):
502             def note_info (m):
503                 self.results[oldnew][m.group(1)] = float (m.group (2))
504             
505             re.sub ('([a-z]+): ([-0-9.]+)\n',
506                     note_info, self.contents[oldnew])
507
508         dist = 0.0
509         factor = {'time': 1.0 ,
510                   'cells': 10.0,
511                   }
512         
513         for k in ('time', 'cells'):
514             dist += math.exp (self.get_ratio (k) * factor[k]) - 1
515
516         dist = min (dist, 100)
517         return dist
518
519     
520 class MidiFileLink (FileCompareLink):
521     def get_content (self, f):
522         s = FileCompareLink.get_content (self, f)
523         s = re.sub ('LilyPond [0-9.]+', '', s)
524         return s
525     
526     def get_cell (self, oldnew):
527         str = ''
528         if oldnew == 1 and self.distance () > 0:
529             str = 'changed' 
530         return str
531     
532
533 class SignatureFileLink (FileLink):
534     def __init__ (self, f1, f2 ):
535         FileLink.__init__ (self, f1, f2)
536         self.system_links = {}
537
538     def add_system_link (self, link, number):
539         self.system_links[number] = link
540
541     def calc_distance (self):
542         d = 0.0
543
544         orphan_distance = 0.0
545         for l in self.system_links.values ():
546             d = max (d, l.geometric_distance ())
547             orphan_distance += l.orphan_count ()
548             
549         return d + orphan_distance
550
551     def source_file (self):
552         for ext in ('.ly', '.ly.txt'):
553             if os.path.exists (self.base_names[1] + ext):
554                 return self.base_names[1] + ext
555         return ''
556     
557     def add_file_compare (self, f1, f2):
558         system_index = [] 
559
560         def note_system_index (m):
561             system_index.append (int (m.group (1)))
562             return ''
563         
564         base1 = re.sub ("-([0-9]+).signature", note_system_index, f1)
565         base2 = re.sub ("-([0-9]+).signature", note_system_index, f2)
566
567         self.base_names = (os.path.normpath (base1),
568                            os.path.normpath (base2))
569
570         def note_original (match):
571             hash_to_original_name[os.path.basename (self.base_names[1])] = match.group (1)
572             return ''
573         
574         ## ugh: drop the .ly.txt
575         for ext in ('.ly', '.ly.txt'):
576             try:
577                 re.sub (r'\\sourcefilename "([^"]+)"',
578                         note_original, open (base1 + ext).read ())
579             except IOError:
580                 pass
581                 
582         s1 = read_signature_file (f1)
583         s2 = read_signature_file (f2)
584
585         link = SystemLink (s1, s2)
586
587         self.add_system_link (link, system_index[0])
588
589     
590     def create_images (self, dest_dir):
591
592         files_created = [[], []]
593         for oldnew in (0, 1):
594             pat = self.base_names[oldnew] + '.eps'
595
596             for f in glob.glob (pat):
597                 infile = f
598                 outfile = (dest_dir + '/' + f).replace ('.eps', '.png')
599
600                 mkdir (os.path.split (outfile)[0])
601                 cmd = ('gs -sDEVICE=png16m -dGraphicsAlphaBits=4 -dTextAlphaBits=4 '
602                        ' -r101 '
603                        ' -sOutputFile=%(outfile)s -dNOSAFER -dEPSCrop -q -dNOPAUSE '
604                        ' %(infile)s  -c quit '  % locals ())
605
606                 files_created[oldnew].append (outfile)
607                 system (cmd)
608
609         return files_created
610     
611     def link_files_for_html (self, dest_dir):
612         FileLink.link_files_for_html (self, dest_dir)
613         to_compare = [[], []]
614
615         exts = []
616         if options.create_images:
617             to_compare = self.create_images (dest_dir)
618         else:
619             exts += ['.png', '-page*png']
620         
621         for ext in exts:            
622             for oldnew in (0,1):
623                 for f in glob.glob (self.base_names[oldnew] + ext):
624                     dst = dest_dir + '/' + f
625                     link_file (f, dst)
626
627                     if f.endswith ('.png'):
628                         to_compare[oldnew].append (f)
629                         
630         if options.compare_images:                
631             for (old, new) in zip (to_compare[0], to_compare[1]):
632                 compare_png_images (old, new, dest_dir)
633
634
635     def get_cell (self, oldnew):
636         def img_cell (ly, img, name):
637             if not name:
638                 name = 'source'
639             else:
640                 name = '<tt>%s</tt>' % name
641                 
642             return '''
643 <a href="%(img)s">
644 <img src="%(img)s" style="border-style: none; max-width: 500px;">
645 </a><br>
646 ''' % locals ()
647         def multi_img_cell (ly, imgs, name):
648             if not name:
649                 name = 'source'
650             else:
651                 name = '<tt>%s</tt>' % name
652
653             imgs_str = '\n'.join (['''<a href="%s">
654 <img src="%s" style="border-style: none; max-width: 500px;">
655 </a><br>''' % (img, img) 
656                                   for img in imgs])
657
658
659             return '''
660 %(imgs_str)s
661 ''' % locals ()
662
663
664
665         def cell (base, name):
666             pat = base + '-page*.png'
667             pages = glob.glob (pat)
668
669             if pages:
670                 return multi_img_cell (base + '.ly', sorted (pages), name)
671             else:
672                 return img_cell (base + '.ly', base + '.png', name)
673
674
675
676         str = cell (os.path.splitext (self.file_names[oldnew])[0], self.name ())  
677         if options.compare_images and oldnew == 1:
678             str = str.replace ('.png', '.compare.jpeg')
679             
680         return str
681
682
683     def get_distance_details (self):
684         systems = self.system_links.items ()
685         systems.sort ()
686
687         html = ""
688         for (c, link) in systems:
689             e = '<td>%d</td>' % c
690             for d in link.distance ():
691                 e += '<td>%f</td>' % d
692             
693             e = '<tr>%s</tr>' % e
694
695             html += e
696
697             e = '<td>%d</td>' % c
698             for s in (link.output_expression_details_string (),
699                       link.orphan_details_string (),
700                       link.geo_details_string ()):
701                 e += "<td>%s</td>" % s
702
703             
704             e = '<tr>%s</tr>' % e
705             html += e
706             
707         original = self.name ()
708         html = '''<html>
709 <head>
710 <title>comparison details for %(original)s</title>
711 </head>
712 <body>
713 <table border=1>
714 <tr>
715 <th>system</th>
716 <th>output</th>
717 <th>orphan</th>
718 <th>geo</th>
719 </tr>
720
721 %(html)s
722 </table>
723
724 </body>
725 </html>
726 ''' % locals ()
727         return html
728
729
730 ################################################################
731 # Files/directories
732
733 import glob
734 import re
735
736 def compare_signature_files (f1, f2):
737     s1 = read_signature_file (f1)
738     s2 = read_signature_file (f2)
739     
740     return SystemLink (s1, s2).distance ()
741
742 def paired_files (dir1, dir2, pattern):
743     """
744     Search DIR1 and DIR2 for PATTERN.
745
746     Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
747
748     """
749
750     files = []
751     for d in (dir1,dir2):
752         found = [os.path.split (f)[1] for f in glob.glob (d + '/' + pattern)]
753         found = dict ((f, 1) for f in found)
754         files.append (found)
755         
756     pairs = []
757     missing = []
758     for f in files[0].keys ():
759         try:
760             files[1].pop (f)
761             pairs.append (f)
762         except KeyError:
763             missing.append (f)
764
765     return (pairs, files[1].keys (), missing)
766     
767 class ComparisonData:
768     def __init__ (self):
769         self.result_dict = {}
770         self.missing = []
771         self.added = []
772         self.file_links = {}
773
774     def compare_trees (self, dir1, dir2):
775         self.compare_directories (dir1, dir2)
776         
777         (root, dirs, files) = os.walk (dir1).next ()
778         for d in dirs:
779             d1 = os.path.join (dir1, d)
780             d2 = os.path.join (dir2, d)
781
782             if os.path.islink (d1) or os.path.islink (d2):
783                 continue
784             
785             if os.path.isdir (d2):
786                 self.compare_trees (d1, d2)
787     
788     def compare_directories (self, dir1, dir2):
789         for ext in ['signature', 'midi', 'log', 'profile', 'gittxt']:
790             (paired, m1, m2) = paired_files (dir1, dir2, '*.' + ext)
791
792             self.missing += [(dir1, m) for m in m1] 
793             self.added += [(dir2, m) for m in m2] 
794
795             for p in paired:
796                 if (options.max_count
797                     and len (self.file_links) > options.max_count):
798                     continue
799                 
800                 f2 = dir2 +  '/' + p
801                 f1 = dir1 +  '/' + p
802                 self.compare_files (f1, f2)
803
804     def compare_files (self, f1, f2):
805         if f1.endswith ('signature'):
806             self.compare_signature_files (f1, f2)
807         else:
808             ext = os.path.splitext (f1)[1]
809             klasses = {
810                 '.midi': MidiFileLink,
811                 '.log' : TextFileCompareLink,
812                 '.profile': ProfileFileLink,
813                 '.gittxt': GitFileCompareLink, 
814                 }
815             
816             if klasses.has_key (ext):
817                 self.compare_general_files (klasses[ext], f1, f2)
818
819     def compare_general_files (self, klass, f1, f2):
820         name = os.path.split (f1)[1]
821
822         file_link = klass (f1, f2)
823         self.file_links[name] = file_link
824         
825     def compare_signature_files (self, f1, f2):
826         name = os.path.split (f1)[1]
827         name = re.sub ('-[0-9]+.signature', '', name)
828         
829         file_link = None
830         try:
831             file_link = self.file_links[name]
832         except KeyError:
833             generic_f1 = re.sub ('-[0-9]+.signature', '.ly', f1)
834             generic_f2 = re.sub ('-[0-9]+.signature', '.ly', f2)
835             file_link = SignatureFileLink (generic_f1, generic_f2)
836             self.file_links[name] = file_link
837
838         file_link.add_file_compare (f1, f2)
839
840     def write_changed (self, dest_dir, threshold):
841         (changed, below, unchanged) = self.thresholded_results (threshold)
842
843         str = '\n'.join ([os.path.splitext (link.file_names[1])[0]
844                         for link in changed])
845         fn = dest_dir + '/changed.txt'
846         
847         open_write_file (fn).write (str)
848                 
849     def thresholded_results (self, threshold):
850         ## todo: support more scores.
851         results = [(link.distance(), link)
852                    for link in self.file_links.values ()]
853         results.sort ()
854         results.reverse ()
855
856         unchanged = [r for (d,r) in results if d == 0.0]
857         below = [r for (d,r) in results if threshold >= d > 0.0]
858         changed = [r for (d,r) in results if d > threshold]
859
860         return (changed, below, unchanged)
861                 
862     def write_text_result_page (self, filename, threshold):
863         out = None
864         if filename == '':
865             out = sys.stdout
866         else:
867             print 'writing "%s"' % filename
868             out = open_write_file (filename)
869
870         (changed, below, unchanged) = self.thresholded_results (threshold)
871
872         
873         for link in changed:
874             out.write (link.text_record_string ())
875
876         out.write ('\n\n')
877         out.write ('%d below threshold\n' % len (below))
878         out.write ('%d unchanged\n' % len (unchanged))
879         
880     def create_text_result_page (self, dir1, dir2, dest_dir, threshold):
881         self.write_text_result_page (dest_dir + '/index.txt', threshold)
882         
883     def create_html_result_page (self, dir1, dir2, dest_dir, threshold):
884         dir1 = dir1.replace ('//', '/')
885         dir2 = dir2.replace ('//', '/')
886
887         (changed, below, unchanged) = self.thresholded_results (threshold)
888
889
890         html = ''
891         old_prefix = os.path.split (dir1)[1]
892         for link in changed:
893             html += link.html_record_string (dest_dir)
894
895
896         short_dir1 = shorten_string (dir1)
897         short_dir2 = shorten_string (dir2)
898         html = '''<html>
899 <table rules="rows" border bordercolor="blue">
900 <tr>
901 <th>distance</th>
902 <th>%(short_dir1)s</th>
903 <th>%(short_dir2)s</th>
904 </tr>
905 %(html)s
906 </table>
907 </html>''' % locals()
908
909         html += ('<p>')
910         below_count = len (below)
911
912         if below_count:
913             html += ('<p>%d below threshold</p>' % below_count)
914             
915         html += ('<p>%d unchanged</p>' % len (unchanged))
916
917         dest_file = dest_dir + '/index.html'
918         open_write_file (dest_file).write (html)
919         
920     def print_results (self, threshold):
921         self.write_text_result_page ('', threshold)
922
923 def compare_trees (dir1, dir2, dest_dir, threshold):
924     data = ComparisonData ()
925     data.compare_trees (dir1, dir2)
926     data.print_results (threshold)
927
928     if os.path.isdir (dest_dir):
929         system ('rm -rf %s '% dest_dir)
930
931     data.write_changed (dest_dir, threshold)
932     data.create_html_result_page (dir1, dir2, dest_dir, threshold)
933     data.create_text_result_page (dir1, dir2, dest_dir, threshold)
934     
935 ################################################################
936 # TESTING
937
938 def mkdir (x):
939     if not os.path.isdir (x):
940         print 'mkdir', x
941         os.makedirs (x)
942
943 def link_file (x, y):
944     mkdir (os.path.split (y)[0])
945     try:
946         print x, '->', y
947         os.link (x, y)
948     except OSError, z:
949         print 'OSError', x, y, z
950         raise OSError
951     
952 def open_write_file (x):
953     d = os.path.split (x)[0]
954     mkdir (d)
955     return open (x, 'w')
956
957
958 def system (x):
959     
960     print 'invoking', x
961     stat = os.system (x)
962     assert stat == 0
963
964
965 def test_paired_files ():
966     print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
967                         os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
968                   
969     
970 def test_compare_trees ():
971     system ('rm -rf dir1 dir2')
972     system ('mkdir dir1 dir2')
973     system ('cp 20{-*.signature,.ly,.png,.eps,.log,.profile} dir1')
974     system ('cp 20{-*.signature,.ly,.png,.eps,.log,.profile} dir2')
975     system ('cp 20expr{-*.signature,.ly,.png,.eps,.log,.profile} dir1')
976     system ('cp 19{-*.signature,.ly,.png,.eps,.log,.profile} dir2/')
977     system ('cp 19{-*.signature,.ly,.png,.eps,.log,.profile} dir1/')
978     system ('cp 19-1.signature 19.sub-1.signature')
979     system ('cp 19.ly 19.sub.ly')
980     system ('cp 19.profile 19.sub.profile')
981     system ('cp 19.log 19.sub.log')
982     system ('cp 19.png 19.sub.png')
983     system ('cp 19.eps 19.sub.eps')
984
985     system ('cp 20multipage* dir1')
986     system ('cp 20multipage* dir2')
987     system ('cp 19multipage-1.signature dir2/20multipage-1.signature')
988
989     
990     system ('mkdir -p dir1/subdir/ dir2/subdir/')
991     system ('cp 19.sub{-*.signature,.ly,.png,.eps,.log,.profile} dir1/subdir/')
992     system ('cp 19.sub{-*.signature,.ly,.png,.eps,.log,.profile} dir2/subdir/')
993     system ('cp 20grob{-*.signature,.ly,.png,.eps,.log,.profile} dir2/')
994     system ('cp 20grob{-*.signature,.ly,.png,.eps,.log,.profile} dir1/')
995     system ('echo HEAD is 1 > dir1/tree.gittxt')
996     system ('echo HEAD is 2 > dir2/tree.gittxt')
997
998     ## introduce differences
999     system ('cp 19-1.signature dir2/20-1.signature')
1000     system ('cp 19.profile dir2/20.profile')
1001     system ('cp 19.png dir2/20.png')
1002     system ('cp 19multipage-page1.png dir2/20multipage-page1.png')
1003     system ('cp 20-1.signature dir2/subdir/19.sub-1.signature')
1004     system ('cp 20.png dir2/subdir/19.sub.png')
1005     system ("sed 's/: /: 1/g'  20.profile > dir2/subdir/19.sub.profile")
1006
1007     ## radical diffs.
1008     system ('cp 19-1.signature dir2/20grob-1.signature')
1009     system ('cp 19-1.signature dir2/20grob-2.signature')
1010     system ('cp 19multipage.midi dir1/midi-differ.midi')
1011     system ('cp 20multipage.midi dir2/midi-differ.midi')
1012     system ('cp 19multipage.log dir1/log-differ.log')
1013     system ('cp 19multipage.log dir2/log-differ.log &&  echo different >> dir2/log-differ.log &&  echo different >> dir2/log-differ.log')
1014
1015     compare_trees ('dir1', 'dir2', 'compare-dir1dir2', options.threshold)
1016
1017
1018 def test_basic_compare ():
1019     ly_template = r"""
1020
1021 \version "2.10.0"
1022 #(define default-toplevel-book-handler
1023   print-book-with-defaults-as-systems )
1024
1025 #(ly:set-option (quote no-point-and-click))
1026
1027 \sourcefilename "my-source.ly"
1028  
1029 %(papermod)s
1030 \header { tagline = ##f }
1031 \score {
1032 <<
1033 \new Staff \relative c {
1034   c4^"%(userstring)s" %(extragrob)s
1035   }
1036 \new Staff \relative c {
1037   c4^"%(userstring)s" %(extragrob)s
1038   }
1039 >>
1040 \layout{}
1041 }
1042
1043 """
1044
1045     dicts = [{ 'papermod' : '',
1046                'name' : '20',
1047                'extragrob': '',
1048                'userstring': 'test' },
1049              { 'papermod' : '#(set-global-staff-size 19.5)',
1050                'name' : '19',
1051                'extragrob': '',
1052                'userstring': 'test' },
1053              { 'papermod' : '',
1054                'name' : '20expr',
1055                'extragrob': '',
1056                'userstring': 'blabla' },
1057              { 'papermod' : '',
1058                'name' : '20grob',
1059                'extragrob': 'r2. \\break c1',
1060                'userstring': 'test' },
1061              ]
1062
1063     for d in dicts:
1064         open (d['name'] + '.ly','w').write (ly_template % d)
1065         
1066     names = [d['name'] for d in dicts]
1067
1068     system ('lilypond -ddump-profile -dseparate-log-files -ddump-signatures --png -b eps ' + ' '.join (names))
1069     
1070
1071     multipage_str = r'''
1072     #(set-default-paper-size "a6")
1073     \score {
1074       \relative {c1 \pageBreak c1 }
1075       \layout {}
1076       \midi {}
1077     }
1078     '''
1079
1080     open ('20multipage.ly', 'w').write (multipage_str.replace ('c1', 'd1'))
1081     open ('19multipage.ly', 'w').write ('#(set-global-staff-size 19.5)\n' + multipage_str)
1082     system ('lilypond -dseparate-log-files -ddump-signatures --png 19multipage 20multipage ')
1083  
1084     test_compare_signatures (names)
1085     
1086 def test_compare_signatures (names, timing=False):
1087
1088     import time
1089
1090     times = 1
1091     if timing:
1092         times = 100
1093
1094     t0 = time.clock ()
1095
1096     count = 0
1097     for t in range (0, times):
1098         sigs = dict ((n, read_signature_file ('%s-1.signature' % n)) for n in names)
1099         count += 1
1100
1101     if timing:
1102         print 'elapsed', (time.clock() - t0)/count
1103
1104
1105     t0 = time.clock ()
1106     count = 0
1107     combinations = {}
1108     for (n1, s1) in sigs.items():
1109         for (n2, s2) in sigs.items():
1110             combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
1111             count += 1
1112
1113     if timing:
1114         print 'elapsed', (time.clock() - t0)/count
1115
1116     results = combinations.items ()
1117     results.sort ()
1118     for k,v in results:
1119         print '%-20s' % k, v
1120
1121     assert combinations['20-20'] == (0.0,0.0,0.0)
1122     assert combinations['20-20expr'][0] > 0.0
1123     assert combinations['20-19'][2] < 10.0
1124     assert combinations['20-19'][2] > 0.0
1125
1126
1127 def run_tests ():
1128     dir = 'test-output-distance'
1129
1130     do_clean = not os.path.exists (dir)
1131
1132     print 'test results in ', dir
1133     if do_clean:
1134         system ('rm -rf ' + dir)
1135         system ('mkdir ' + dir)
1136         
1137     os.chdir (dir)
1138     if do_clean:
1139         test_basic_compare ()
1140         
1141     test_compare_trees ()
1142     
1143 ################################################################
1144 #
1145
1146 def main ():
1147     p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
1148     p.usage = 'output-distance.py [options] tree1 tree2'
1149     
1150     p.add_option ('', '--test-self',
1151                   dest="run_test",
1152                   action="store_true",
1153                   help='run test method')
1154     
1155     p.add_option ('--max-count',
1156                   dest="max_count",
1157                   metavar="COUNT",
1158                   type="int",
1159                   default=0, 
1160                   action="store",
1161                   help='only analyze COUNT signature pairs')
1162
1163     p.add_option ('', '--threshold',
1164                   dest="threshold",
1165                   default=0.3,
1166                   action="store",
1167                   type="float",
1168                   help='threshold for geometric distance')
1169
1170     p.add_option ('--no-compare-images',
1171                   dest="compare_images",
1172                   default=True,
1173                   action="store_false",
1174                   help="Don't run graphical comparisons")
1175
1176     p.add_option ('--create-images',
1177                   dest="create_images",
1178                   default=False,
1179                   action="store_true",
1180                   help="Create PNGs from EPSes")
1181
1182     p.add_option ('-o', '--output-dir',
1183                   dest="output_dir",
1184                   default=None,
1185                   action="store",
1186                   type="string",
1187                   help='where to put the test results [tree2/compare-tree1tree2]')
1188
1189     global options
1190     (options, a) = p.parse_args ()
1191
1192     if options.run_test:
1193         run_tests ()
1194         sys.exit (0)
1195
1196     if len (a) != 2:
1197         p.print_usage()
1198         sys.exit (2)
1199
1200     name = options.output_dir
1201     if not name:
1202         name = a[0].replace ('/', '')
1203         name = os.path.join (a[1], 'compare-' + shorten_string (name))
1204     
1205     compare_trees (a[0], a[1], name, options.threshold)
1206
1207 if __name__ == '__main__':
1208     main()
1209