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