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