]> git.donarmstrong.com Git - lilypond.git/blob - buildscripts/output-distance.py
Merge branch 'master' of ssh+git://hanwen@git.sv.gnu.org/srv/git/lilypond
[lilypond.git] / buildscripts / output-distance.py
1 #!@TARGET_PYTHON@
2 import sys
3 import optparse
4 import os
5
6 ## so we can call directly as buildscripts/output-distance.py
7 me_path = os.path.abspath (os.path.split (sys.argv[0])[0])
8 sys.path.insert (0, me_path + '/../python/')
9
10
11 import safeeval
12
13
14 X_AXIS = 0
15 Y_AXIS = 1
16 INFTY = 1e6
17
18 OUTPUT_EXPRESSION_PENALTY = 1
19 ORPHAN_GROB_PENALTY = 1
20 options = None
21
22 def shorten_string (s):
23     threshold = 15 
24     if len (s) > 2*threshold:
25         s = s[:threshold] + '..' + s[-threshold:]
26     return s
27
28 def max_distance (x1, x2):
29     dist = 0.0
30
31     for (p,q) in zip (x1, x2):
32         dist = max (abs (p-q), dist)
33         
34     return dist
35
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 def read_pipe (c):
278     print 'pipe' , c
279     return os.popen (c).read ()
280
281 def system (c):
282     print 'system' , c
283     s = os.system (c)
284     if s :
285         raise Exception ("failed")
286     return
287
288 def compare_png_images (old, new, dir):
289     def png_dims (f):
290         m = re.search ('([0-9]+) x ([0-9]+)', read_pipe ('file %s' % f))
291         
292         return tuple (map (int, m.groups ()))
293
294     dest = os.path.join (dir, new.replace ('.png', '.compare.jpeg'))
295     try:
296         dims1 = png_dims (old)
297         dims2 = png_dims (new)
298     except AttributeError:
299         ## hmmm. what to do?
300         system ('touch %(dest)s' % locals ())
301         return
302     
303     dims = (min (dims1[0], dims2[0]),
304             min (dims1[1], dims2[1]))
305
306     system ('convert -depth 8 -crop %dx%d+0+0 %s crop1.png' % (dims + (old,)))
307     system ('convert -depth 8 -crop %dx%d+0+0 %s crop2.png' % (dims + (new,)))
308
309     system ('compare -depth 8 crop1.png crop2.png diff.png')
310
311     system ("convert  -depth 8 diff.png -blur 0x3 -negate -channel alpha,blue -type TrueColorMatte -fx 'intensity'    matte.png")
312
313     system ("composite -quality 65 matte.png %(new)s %(dest)s" % locals ())
314
315 class FileLink:
316     def __init__ (self):
317         self._distance = None
318
319     def text_record_string (self):
320         return '%-30f %-20s\n' % (self.distance (),
321                                   self.name ())
322     def calc_distance (self):
323         return 0.0
324
325     def distance (self):
326         if self._distance == None:
327            self._distance = self.calc_distance ()
328
329         return self._distance
330     
331     def name (self):
332         return ''
333     
334     def link_files_for_html (self, old_dir, new_dir, dest_dir):
335         pass
336
337     def write_html_system_details (self, dir1, dir2, dest_dir):
338         pass
339         
340     def html_record_string (self,  old_dir, new_dir):
341         return ''
342
343 class FileCompareLink (FileLink):
344     def __init__ (self, f1, f2):
345         FileLink.__init__ (self)
346         self.files = (f1, f2)
347         self.contents = (self.get_content (self.files[0]),
348                          self.get_content (self.files[1]))
349         
350     def link_files_for_html (self, old_dir, new_dir, dest_dir):
351         for f in self.files:
352             link_file (f, os.path.join (dest_dir, f))
353
354     def name (self):
355         name = os.path.basename (self.files[0])
356         name = os.path.splitext (name)[0]
357         return name
358         
359     def calc_distance (self):
360         ## todo: could use import MIDI to pinpoint
361         ## what & where changed.
362
363         if self.contents[0] == self.contents[1]:
364             return 0.0
365         else:
366             return 100.0;
367         
368     def html_record_string (self, d1, d2):
369         (dist, f1, f2) = (self.distance(),) + self.files
370         
371         return '''<tr>
372 <td>
373 %(dist)f
374 </td>
375 <td><a href="%(f1)s"><tt>%(f1)s</tt></td>
376 <td><a href="%(f2)s"><tt>%(f2)s</tt></td>
377 </tr>''' % locals ()
378
379     def get_content (self, f):
380         print 'reading', f
381         s = open (f).read ()
382         return s
383     
384 class ProfileFileLink (FileCompareLink):
385     def calc_distance (self):
386         r = [{},{}]
387         for oldnew in (0,1):
388             def note_info (m):
389                 r[oldnew][m.group(1)] = float (m.group (2))
390             
391             re.sub ('([a-z]+): ([-0-9.]+)\n',
392                     note_info, self.contents[oldnew])
393
394         dist = 0.0
395         for k in ('time', 'cells'):
396             (v1,v2) = (r[0].get (k , -1),
397                        r[1].get (k , -1))
398             if v1 < 0 or v2 < 0 or float (v1 + v2) == 0.0:
399                 continue
400
401             ratio = v2 / float (v1+v2)
402             if ratio < 0.25 or ratio > 0.75:
403                 dist += 1
404
405         return dist
406
407         
408 class TextFileCompareLink (FileCompareLink):
409     def calc_distance (self):
410         import difflib
411         diff = difflib.unified_diff (self.contents[0].strip().split ('\n'),
412                                      self.contents[1].strip().split ('\n'),
413                                      fromfiledate = self.files[0],
414                                      tofiledate = self.files[1]
415                                      )
416
417         self.diff_lines =  [l for l in diff]
418         return float (len (self.diff_lines))
419         
420     def link_files_for_html (self, old_dir, new_dir, dest_dir):
421         str = '\n'.join ([d.replace ('\n','') for d in self.diff_lines])
422         f = os.path.join (new_dir, self.name ()) + '.diff.txt'
423         f = os.path.join (dest_dir, f)
424         open_write_file (f).write (str)
425      
426     def html_record_string (self, d1, d2):
427         return '''<tr>
428 <td>
429 %f
430 </td>
431 <td><tt>%s</tt></td>
432 <td><a href="%s.diff.txt"><tt>%s</tt></a></td>
433 </tr>''' % (self.distance(),
434             self.files[0],
435             os.path.join (d2, self.name ()),
436             self.files[1])
437
438 class MidiFileLink (FileCompareLink):
439     def get_content (self, f):
440         s = FileCompareLink.get_content (self, f)
441         s = re.sub ('LilyPond [0-9.]+', '', s)
442         return s
443
444 class SignatureFileLink (FileLink):
445     def __init__ (self):
446         FileLink.__init__ (self)
447         self.original_name = ''
448         self.base_names = ('','')
449         self.system_links = {}
450         
451     def name (self):
452         return os.path.splitext (self.original_name)[0]
453     
454     def add_system_link (self, link, number):
455         self.system_links[number] = link
456
457     def calc_distance (self):
458         d = 0.0
459
460         orphan_distance = 0.0
461         for l in self.system_links.values ():
462             d = max (d, l.geometric_distance ())
463             orphan_distance += l.orphan_count ()
464             
465         return d + orphan_distance
466
467     def source_file (self):
468         for ext in ('.ly', '.ly.txt'):
469             if os.path.exists (self.base_names[1] + ext):
470                 return self.base_names[1] + ext
471         return ''
472     
473     def add_file_compare (self, f1, f2):
474         system_index = [] 
475
476         def note_system_index (m):
477             system_index.append (int (m.group (1)))
478             return ''
479         
480         base1 = re.sub ("-([0-9]+).signature", note_system_index, f1)
481         base2 = re.sub ("-([0-9]+).signature", note_system_index, f2)
482
483         self.base_names = (os.path.normpath (base1),
484                            os.path.normpath (base2))
485
486         def note_original (match):
487             self.original_name = match.group (1)
488             return ''
489         
490         if not self.original_name:
491             self.original_name = os.path.split (base1)[1]
492
493             ## ugh: drop the .ly.txt
494             for ext in ('.ly', '.ly.txt'):
495                 try:
496                     re.sub (r'\\sourcefilename "([^"]+)"',
497                             note_original, open (base1 + ext).read ())
498                 except IOError:
499                     pass
500                 
501         s1 = read_signature_file (f1)
502         s2 = read_signature_file (f2)
503
504         link = SystemLink (s1, s2)
505
506         self.add_system_link (link, system_index[0])
507
508     
509     def create_images (self, old_dir, new_dir, dest_dir):
510
511         files_created = [[], []]
512         for oldnew in (0, 1):
513             pat = self.base_names[oldnew] + '.eps'
514
515             for f in glob.glob (pat):
516                 infile = f
517                 outfile = (dest_dir + '/' + f).replace ('.eps', '.png')
518
519                 mkdir (os.path.split (outfile)[0])
520                 cmd = ('gs -sDEVICE=png16m -dGraphicsAlphaBits=4 -dTextAlphaBits=4 '
521                        ' -r101 '
522                        ' -sOutputFile=%(outfile)s -dNOSAFER -dEPSCrop -q -dNOPAUSE '
523                        ' %(infile)s  -c quit '  % locals ())
524
525                 files_created[oldnew].append (outfile)
526                 system (cmd)
527
528         return files_created
529     
530     def link_files_for_html (self, old_dir, new_dir, dest_dir):
531         to_compare = [[], []]
532
533         exts = ['.ly']
534         if options.create_images:
535             to_compare = self.create_images (old_dir, new_dir, dest_dir)
536         else:
537             exts += ['.png', '-page*png']
538         
539         for ext in exts:            
540             for oldnew in (0,1):
541                 for f in glob.glob (self.base_names[oldnew] + ext):
542                     dst = dest_dir + '/' + f
543                     link_file (f, dst)
544
545                     if f.endswith ('.png'):
546                         to_compare[oldnew].append (f)
547                         
548         if options.compare_images:                
549             for (old, new) in zip (to_compare[0], to_compare[1]):
550                 compare_png_images (old, new, dest_dir)
551
552                 
553     def html_record_string (self,  old_dir, new_dir):
554         def img_cell (ly, img, name):
555             if not name:
556                 name = 'source'
557             else:
558                 name = '<tt>%s</tt>' % name
559                 
560             return '''
561 <td align="center">
562 <a href="%(img)s">
563 <img src="%(img)s" style="border-style: none; max-width: 500px;">
564 </a><br>
565 <font size="-2">(<a href="%(ly)s">%(name)s</a>)
566 </font>
567 </td>
568 ''' % locals ()
569         def multi_img_cell (ly, imgs, name):
570             if not name:
571                 name = 'source'
572             else:
573                 name = '<tt>%s</tt>' % name
574
575             imgs_str = '\n'.join (['''<a href="%s">
576 <img src="%s" style="border-style: none; max-width: 500px;">
577 </a><br>''' % (img, img) 
578                                   for img in imgs])
579
580
581             return '''
582 <td align="center">
583 %(imgs_str)s
584 <font size="-2">(<a href="%(ly)s">%(name)s</a>)
585 </font>
586 </td>
587 ''' % locals ()
588
589
590
591         def cell (base, name):
592             pat = base + '-page*.png'
593             pages = glob.glob (pat)
594
595             if pages:
596                 return multi_img_cell (base + '.ly', sorted (pages), name)
597             else:
598                 return img_cell (base + '.ly', base + '.png', name)
599             
600
601         html_2  = self.base_names[1] + '.html'
602         name = self.original_name
603
604         cell_1 = cell (self.base_names[0], name)
605         cell_2 = cell (self.base_names[1], name)
606         if options.compare_images:
607             cell_2 = cell_2.replace ('.png', '.compare.jpeg')
608         
609         html_entry = '''
610 <tr>
611 <td>
612 %f<br>
613 (<a href="%s">details</a>)
614 </td>
615
616 %s
617 %s
618 </tr>
619 ''' % (self.distance (), html_2, cell_1, cell_2)
620
621         return html_entry
622
623
624     def html_system_details_string (self):
625         systems = self.system_links.items ()
626         systems.sort ()
627
628         html = ""
629         for (c, link) in systems:
630             e = '<td>%d</td>' % c
631             for d in link.distance ():
632                 e += '<td>%f</td>' % d
633             
634             e = '<tr>%s</tr>' % e
635
636             html += e
637
638             e = '<td>%d</td>' % c
639             for s in (link.output_expression_details_string (),
640                       link.orphan_details_string (),
641                       link.geo_details_string ()):
642                 e += "<td>%s</td>" % s
643
644             
645             e = '<tr>%s</tr>' % e
646             html += e
647             
648         original = self.original_name
649         html = '''<html>
650 <head>
651 <title>comparison details for %(original)s</title>
652 </head>
653 <body>
654 <table border=1>
655 <tr>
656 <th>system</th>
657 <th>output</th>
658 <th>orphan</th>
659 <th>geo</th>
660 </tr>
661
662 %(html)s
663 </table>
664
665 </body>
666 </html>
667 ''' % locals ()
668         return html
669
670     def write_html_system_details (self, dir1, dir2, dest_dir):
671         dest_file =  os.path.join (dest_dir, self.base_names[1] + '.html')
672
673         details = open_write_file (dest_file)
674         details.write (self.html_system_details_string ())
675
676 ################################################################
677 # Files/directories
678
679 import glob
680 import re
681
682 def compare_signature_files (f1, f2):
683     s1 = read_signature_file (f1)
684     s2 = read_signature_file (f2)
685     
686     return SystemLink (s1, s2).distance ()
687
688 def paired_files (dir1, dir2, pattern):
689     """
690     Search DIR1 and DIR2 for PATTERN.
691
692     Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
693
694     """
695     
696     files1 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir1 + '/' + pattern))
697     files2 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir2 + '/' + pattern))
698
699     pairs = []
700     missing = []
701     for f in files1.keys ():
702         try:
703             files2.pop (f)
704             pairs.append (f)
705         except KeyError:
706             missing.append (f)
707
708     return (pairs, files2.keys (), missing)
709     
710 class ComparisonData:
711     def __init__ (self):
712         self.result_dict = {}
713         self.missing = []
714         self.added = []
715         self.file_links = {}
716
717     def compare_trees (self, dir1, dir2):
718         self.compare_directories (dir1, dir2)
719         
720         (root, dirs, files) = os.walk (dir1).next ()
721         for d in dirs:
722             d1 = os.path.join (dir1, d)
723             d2 = os.path.join (dir2, d)
724
725             if os.path.islink (d1) or os.path.islink (d2):
726                 continue
727             
728             if os.path.isdir (d2):
729                 self.compare_trees (d1, d2)
730     
731     def compare_directories (self, dir1, dir2):
732         for ext in ['signature', 'midi', 'log', 'profile']:
733             (paired, m1, m2) = paired_files (dir1, dir2, '*.' + ext)
734
735             self.missing += [(dir1, m) for m in m1] 
736             self.added += [(dir2, m) for m in m2] 
737
738             for p in paired:
739                 if (options.max_count
740                     and len (self.file_links) > options.max_count):
741                     
742                     continue
743                 
744                 f2 = dir2 +  '/' + p
745                 f1 = dir1 +  '/' + p
746                 self.compare_files (f1, f2)
747
748     def compare_files (self, f1, f2):
749         if f1.endswith ('signature'):
750             self.compare_signature_files (f1, f2)
751         else:
752             ext = os.path.splitext (f1)[1]
753             klasses = {
754                 '.midi': MidiFileLink,
755                 '.log' : TextFileCompareLink,
756                 '.profile': ProfileFileLink,
757                 }
758             
759             if klasses.has_key (ext):
760                 self.compare_general_files (klasses[ext], f1, f2)
761
762     def compare_general_files (self, klass, f1, f2):
763         name = os.path.split (f1)[1]
764
765         file_link = klass (f1, f2)
766         self.file_links[name] = file_link
767         
768     def compare_signature_files (self, f1, f2):
769         name = os.path.split (f1)[1]
770         name = re.sub ('-[0-9]+.signature', '', name)
771         
772         file_link = None
773         try:
774             file_link = self.file_links[name]
775         except KeyError:
776             file_link = SignatureFileLink ()
777             self.file_links[name] = file_link
778
779         file_link.add_file_compare (f1, f2)
780
781     def remove_changed (self, dir, threshold):
782         (changed, below, unchanged) = self.thresholded_results (threshold)
783         for link in changed:
784             try:
785                 system ('rm -f %s*' % link.base_names[1])
786             except AttributeError: ### UGH.
787                 system ('rm -f %s/%s*' % (dir, link.name ()))
788     def thresholded_results (self, threshold):
789         ## todo: support more scores.
790         results = [(link.distance(), link)
791                    for link in self.file_links.values ()]
792         results.sort ()
793         results.reverse ()
794
795         unchanged = [r for (d,r) in results if d == 0.0]
796         below = [r for (d,r) in results if threshold >= d > 0.0]
797         changed = [r for (d,r) in results if d > threshold]
798
799         return (changed, below, unchanged)
800                 
801     def write_text_result_page (self, filename, threshold):
802         out = None
803         if filename == '':
804             out = sys.stdout
805         else:
806             print 'writing "%s"' % filename
807             out = open_write_file (filename)
808
809         (changed, below, unchanged) = self.thresholded_results (threshold)
810
811         
812         for link in changed:
813             out.write (link.text_record_string ())
814
815         out.write ('\n\n')
816         out.write ('%d below threshold\n' % len (below))
817         out.write ('%d unchanged\n' % len (unchanged))
818         
819     def create_text_result_page (self, dir1, dir2, dest_dir, threshold):
820         self.write_text_result_page (dest_dir + '/index.txt', threshold)
821         
822     def create_html_result_page (self, dir1, dir2, dest_dir, threshold):
823         dir1 = dir1.replace ('//', '/')
824         dir2 = dir2.replace ('//', '/')
825
826         (changed, below, unchanged) = self.thresholded_results (threshold)
827
828
829         html = ''
830         old_prefix = os.path.split (dir1)[1]
831         for link in changed:
832             link.link_files_for_html (dir1, dir2, dest_dir) 
833             link.write_html_system_details (dir1, dir2, dest_dir)
834             
835             html += link.html_record_string (dir1, dir2)
836
837
838         short_dir1 = shorten_string (dir1)
839         short_dir2 = shorten_string (dir2)
840         html = '''<html>
841 <table rules="rows" border bordercolor="blue">
842 <tr>
843 <th>distance</th>
844 <th>%(short_dir1)s</th>
845 <th>%(short_dir2)s</th>
846 </tr>
847 %(html)s
848 </table>
849 </html>''' % locals()
850
851         html += ('<p>')
852         below_count = len (below)
853
854         if below_count:
855             html += ('<p>%d below threshold</p>' % below_count)
856             
857         html += ('<p>%d unchanged</p>' % len (unchanged))
858
859         dest_file = dest_dir + '/index.html'
860         open_write_file (dest_file).write (html)
861         
862     def print_results (self, threshold):
863         self.write_text_result_page ('', threshold)
864
865 def compare_trees (dir1, dir2, dest_dir, threshold):
866     data = ComparisonData ()
867     data.compare_trees (dir1, dir2)
868     data.print_results (threshold)
869
870     if options.remove_changed:
871         data.remove_changed (dir2, threshold)
872         return
873     
874     if os.path.isdir (dest_dir):
875         system ('rm -rf %s '% dest_dir)
876
877     data.create_html_result_page (dir1, dir2, dest_dir, threshold)
878     data.create_text_result_page (dir1, dir2, dest_dir, threshold)
879     
880 ################################################################
881 # TESTING
882
883 def mkdir (x):
884     if not os.path.isdir (x):
885         print 'mkdir', x
886         os.makedirs (x)
887
888 def link_file (x, y):
889     mkdir (os.path.split (y)[0])
890     try:
891         os.link (x, y)
892     except OSError, z:
893         print 'OSError', x, y, z
894         raise OSError
895     
896 def open_write_file (x):
897     d = os.path.split (x)[0]
898     mkdir (d)
899     return open (x, 'w')
900
901
902 def system (x):
903     
904     print 'invoking', x
905     stat = os.system (x)
906     assert stat == 0
907
908
909 def test_paired_files ():
910     print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
911                         os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
912                   
913     
914 def test_compare_trees ():
915     system ('rm -rf dir1 dir2')
916     system ('mkdir dir1 dir2')
917     system ('cp 20{-*.signature,.ly,.png,.eps,.log,.profile} dir1')
918     system ('cp 20{-*.signature,.ly,.png,.eps,.log,.profile} dir2')
919     system ('cp 20expr{-*.signature,.ly,.png,.eps,.log,.profile} dir1')
920     system ('cp 19{-*.signature,.ly,.png,.eps,.log,.profile} dir2/')
921     system ('cp 19{-*.signature,.ly,.png,.eps,.log,.profile} dir1/')
922     system ('cp 19-1.signature 19-sub-1.signature')
923     system ('cp 19.ly 19-sub.ly')
924     system ('cp 19.profile 19-sub.profile')
925     system ('cp 19.log 19-sub.log')
926     system ('cp 19.png 19-sub.png')
927     system ('cp 19.eps 19-sub.eps')
928
929     system ('cp 20multipage* dir1')
930     system ('cp 20multipage* dir2')
931     system ('cp 19multipage-1.signature dir2/20multipage-1.signature')
932
933     
934     system ('mkdir -p dir1/subdir/ dir2/subdir/')
935     system ('cp 19-sub{-*.signature,.ly,.png,.eps,.log,.profile} dir1/subdir/')
936     system ('cp 19-sub{-*.signature,.ly,.png,.eps,.log,.profile} dir2/subdir/')
937     system ('cp 20grob{-*.signature,.ly,.png,.eps,.log,.profile} dir2/')
938     system ('cp 20grob{-*.signature,.ly,.png,.eps,.log,.profile} dir1/')
939
940     ## introduce differences
941     system ('cp 19-1.signature dir2/20-1.signature')
942     system ('cp 19.profile dir2/20.profile')
943     system ('cp 19.png dir2/20.png')
944     system ('cp 19multipage-page1.png dir2/20multipage-page1.png')
945     system ('cp 20-1.signature dir2/subdir/19-sub-1.signature')
946     system ('cp 20.png dir2/subdir/19-sub.png')
947
948     ## radical diffs.
949     system ('cp 19-1.signature dir2/20grob-1.signature')
950     system ('cp 19-1.signature dir2/20grob-2.signature')
951     system ('cp 19multipage.midi dir1/midi-differ.midi')
952     system ('cp 20multipage.midi dir2/midi-differ.midi')
953     system ('cp 19multipage.log dir1/log-differ.log')
954     system ('cp 19multipage.log dir2/log-differ.log &&  echo different >> dir2/log-differ.log &&  echo different >> dir2/log-differ.log')
955
956     compare_trees ('dir1', 'dir2', 'compare-dir1dir2', 0.5)
957
958
959 def test_basic_compare ():
960     ly_template = r"""
961
962 \version "2.10.0"
963 #(define default-toplevel-book-handler
964   print-book-with-defaults-as-systems )
965
966 #(ly:set-option (quote no-point-and-click))
967
968 \sourcefilename "my-source.ly"
969  
970 %(papermod)s
971 \header { tagline = ##f }
972 \score {
973 <<
974 \new Staff \relative c {
975   c4^"%(userstring)s" %(extragrob)s
976   }
977 \new Staff \relative c {
978   c4^"%(userstring)s" %(extragrob)s
979   }
980 >>
981 \layout{}
982 }
983
984 """
985
986     dicts = [{ 'papermod' : '',
987                'name' : '20',
988                'extragrob': '',
989                'userstring': 'test' },
990              { 'papermod' : '#(set-global-staff-size 19.5)',
991                'name' : '19',
992                'extragrob': '',
993                'userstring': 'test' },
994              { 'papermod' : '',
995                'name' : '20expr',
996                'extragrob': '',
997                'userstring': 'blabla' },
998              { 'papermod' : '',
999                'name' : '20grob',
1000                'extragrob': 'r2. \\break c1',
1001                'userstring': 'test' },
1002              ]
1003
1004     for d in dicts:
1005         open (d['name'] + '.ly','w').write (ly_template % d)
1006         
1007     names = [d['name'] for d in dicts]
1008     
1009     system ('lilypond -ddump-profile -dseparate-log-files -ddump-signatures --png -b eps ' + ' '.join (names))
1010     
1011
1012     multipage_str = r'''
1013     #(set-default-paper-size "a6")
1014     \score {
1015       \relative {c1 \pageBreak c1 }
1016       \layout {}
1017       \midi {}
1018     }
1019     '''
1020
1021     open ('20multipage', 'w').write (multipage_str.replace ('c1', 'd1'))
1022     open ('19multipage', 'w').write ('#(set-global-staff-size 19.5)\n' + multipage_str)
1023     system ('lilypond -dseparate-log-files -ddump-signatures --png 19multipage 20multipage ')
1024  
1025     test_compare_signatures (names)
1026     
1027 def test_compare_signatures (names, timing=False):
1028
1029     import time
1030
1031     times = 1
1032     if timing:
1033         times = 100
1034
1035     t0 = time.clock ()
1036
1037     count = 0
1038     for t in range (0, times):
1039         sigs = dict ((n, read_signature_file ('%s-1.signature' % n)) for n in names)
1040         count += 1
1041
1042     if timing:
1043         print 'elapsed', (time.clock() - t0)/count
1044
1045
1046     t0 = time.clock ()
1047     count = 0
1048     combinations = {}
1049     for (n1, s1) in sigs.items():
1050         for (n2, s2) in sigs.items():
1051             combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
1052             count += 1
1053
1054     if timing:
1055         print 'elapsed', (time.clock() - t0)/count
1056
1057     results = combinations.items ()
1058     results.sort ()
1059     for k,v in results:
1060         print '%-20s' % k, v
1061
1062     assert combinations['20-20'] == (0.0,0.0,0.0)
1063     assert combinations['20-20expr'][0] > 0.0
1064     assert combinations['20-19'][2] < 10.0
1065     assert combinations['20-19'][2] > 0.0
1066
1067
1068 def run_tests ():
1069     dir = 'test-output-distance'
1070
1071     do_clean = not os.path.exists (dir)
1072
1073     print 'test results in ', dir
1074     if do_clean:
1075         system ('rm -rf ' + dir)
1076         system ('mkdir ' + dir)
1077         
1078     os.chdir (dir)
1079     if do_clean:
1080         test_basic_compare ()
1081         
1082     test_compare_trees ()
1083     
1084 ################################################################
1085 #
1086
1087 def main ():
1088     p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
1089     p.usage = 'output-distance.py [options] tree1 tree2'
1090     
1091     p.add_option ('', '--test-self',
1092                   dest="run_test",
1093                   action="store_true",
1094                   help='run test method')
1095     
1096     p.add_option ('--max-count',
1097                   dest="max_count",
1098                   metavar="COUNT",
1099                   type="int",
1100                   default=0, 
1101                   action="store",
1102                   help='only analyze COUNT signature pairs')
1103
1104     p.add_option ('', '--threshold',
1105                   dest="threshold",
1106                   default=0.3,
1107                   action="store",
1108                   type="float",
1109                   help='threshold for geometric distance')
1110
1111
1112     p.add_option ('--remove-changed',
1113                   dest="remove_changed",
1114                   default=False,
1115                   action="store_true",
1116                   help="Remove all files from tree2 that are over the threshold.")
1117
1118     p.add_option ('--no-compare-images',
1119                   dest="compare_images",
1120                   default=True,
1121                   action="store_false",
1122                   help="Don't run graphical comparisons")
1123
1124     p.add_option ('--create-images',
1125                   dest="create_images",
1126                   default=False,
1127                   action="store_true",
1128                   help="Create PNGs from EPSes")
1129
1130     p.add_option ('-o', '--output-dir',
1131                   dest="output_dir",
1132                   default=None,
1133                   action="store",
1134                   type="string",
1135                   help='where to put the test results [tree2/compare-tree1tree2]')
1136
1137     global options
1138     (options, a) = p.parse_args ()
1139
1140     if options.run_test:
1141         run_tests ()
1142         sys.exit (0)
1143
1144     if len (a) != 2:
1145         p.print_usage()
1146         sys.exit (2)
1147
1148     name = options.output_dir
1149     if not name:
1150         name = a[0].replace ('/', '')
1151         name = os.path.join (a[1], 'compare-' + shorten_string (name))
1152     
1153     compare_trees (a[0], a[1], name, options.threshold)
1154
1155 if __name__ == '__main__':
1156     main()
1157