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