33
44import io
55import sys
6+ import shutil
67import argparse
78import logging
89import threading
@@ -29,6 +30,7 @@ class Config:
2930 comparator = None
3031 browser = None
3132 thread_local = threading .local ()
33+ log_file : Path = None
3234
3335
3436def result_symbol (result : str ) -> str | None :
@@ -293,7 +295,7 @@ def generate_entry(
293295 result += "<td></td>"
294296
295297 if is_comparable :
296- main = f'<a href="/compare/{ path } ">{ name } </a>'
298+ main = f'<a href="/compare/{ path } " target="_blank" >{ name } </a>'
297299 else :
298300 main = f"{ name } "
299301 if cmp_result is not None :
@@ -306,6 +308,10 @@ def generate_entry(
306308 result += f'<td class="main" style="{ style } ">{ main } </td>'
307309 result += f"<td>{ message } </td>"
308310
311+ result += (
312+ f"<td><button onclick=\" updateRef('{ path } ')\" >update ref</button></td>"
313+ )
314+
309315 result += "</tr>"
310316
311317 return result
@@ -444,18 +450,25 @@ def generate_tree(
444450
445451 result += "<p>"
446452 result += "comparing<br>"
447- result += f"A : { Config .path_a } <br>"
448- result += f"B : { Config .path_b } "
453+ result += f"Reference : { Config .path_a } <br>"
454+ result += f"Monitored : { Config .path_b } "
449455 result += "</p>"
450456
457+ if Config .log_file is not None :
458+ result += "<p>"
459+ result += (
460+ f'<a href="/logfile" target="_blank">View log file: { Config .log_file } </a>'
461+ )
462+ result += "</p>"
463+
451464 result += "<p>"
452465 result += '<button onclick="toggleAll(true)">Expand All</button>'
453466 result += '<button onclick="toggleAll(false)">Collapse All</button>'
454467 result += "</p>"
455468
456469 result += "<table>"
457470 result += "<thead>"
458- result += "<tr><td></td><td></td><td>Name</td><td>Message</td></tr>"
471+ result += "<tr><td></td><td></td><td>Name</td><td>Message</td><td>Actions</td>< /tr>"
459472 result += "</thead>"
460473 result += "<tbody>"
461474 result += generate_tree (Config .path_a , Config .path_b , "" , None , 0 )
@@ -494,6 +507,21 @@ def generate_tree(
494507 }
495508 });
496509}
510+
511+ function updateRef(path) {
512+ fetch(`/update_ref/${path}`)
513+ .then(response => {
514+ if (response.ok) {
515+ alert(`Reference updated for ${path}`);
516+ location.reload();
517+ } else {
518+ alert(`Failed to update reference for ${path}: ${response.statusText}`);
519+ }
520+ })
521+ .catch(error => {
522+ alert(`Error updating reference for ${path}: ${error}`);
523+ });
524+ }
497525</script>
498526</body>
499527</html>
@@ -502,6 +530,16 @@ def generate_tree(
502530 return result
503531
504532
533+ @app .route ("/logfile" )
534+ def logfile ():
535+ logger .debug ("Serving log file" )
536+
537+ if Config .log_file is None :
538+ return "No log file configured" , 404
539+
540+ return send_from_directory (Config .log_file .parent , Config .log_file .name )
541+
542+
505543@app .route ("/compare/<path:path>" )
506544def compare (path : str ):
507545 logger .debug (f"Generating comparison page for path: { path } " )
@@ -518,15 +556,15 @@ def compare(path: str):
518556</head>
519557<body style="display:flex;flex-flow:row;">
520558<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
521- <a href="/file/a/{ path } ">{ Config .path_a / path } </a>
559+ <a href="/file/a/{ path } " target="_blank" >{ Config .path_a / path } </a>
522560 <iframe id="a" src="/file/a/{ path } " title="a" frameborder="0" align="left" style="flex:1;"></iframe>
523561</div>
524562<div style="display:flex;flex:0 0 50px;flex-flow:column;">
525- <a href="/image_diff/{ path } ">diff</a>
563+ <a href="/image_diff/{ path } " target="_blank" >diff</a>
526564 <img src="/image_diff/{ path } " width="50" height="0" style="flex:1;">
527565</div>
528566<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
529- <a href="/file/b/{ path } ">{ Config .path_b / path } </a>
567+ <a href="/file/b/{ path } " target="_blank" >{ Config .path_b / path } </a>
530568 <iframe id="b" src="/file/b/{ path } " title="b" frameborder="0" align="right" style="flex:1;"></iframe>
531569</div>
532570<script>
@@ -551,6 +589,9 @@ def image_diff(path: str):
551589 if not isinstance (path , str ):
552590 raise TypeError ("Path must be a string" )
553591
592+ if Config .driver is None :
593+ return "Image diff not available without browser driver" , 404
594+
554595 diff , _ = html_render_diff (
555596 Config .path_a / path ,
556597 Config .path_b / path ,
@@ -575,34 +616,74 @@ def file(variant: str, path: str):
575616 return send_from_directory (variant_root , path )
576617
577618
578- def setup_logging (verbosity : int ):
619+ def verbosity_to_level (verbosity : int ) -> int :
579620 if verbosity >= 3 :
580- level = logging .DEBUG
621+ return logging .DEBUG
581622 elif verbosity == 2 :
582- level = logging .INFO
623+ return logging .INFO
583624 elif verbosity == 1 :
584- level = logging .WARNING
625+ return logging .WARNING
585626 else :
586- level = logging .ERROR
627+ return logging .ERROR
628+
629+
630+ def setup_logging (
631+ verbosity : int , log_file : Path = None , log_file_verbosity : int = None
632+ ) -> None :
633+ level = verbosity_to_level (verbosity )
634+
635+ root_logger = logging .getLogger ()
636+ root_logger .setLevel (level )
637+ root_logger .handlers .clear ()
587638
588639 formatter = logging .Formatter (
589640 fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" ,
590641 datefmt = "%Y-%m-%d %H:%M:%S" ,
591642 )
592643
593644 console_handler = logging .StreamHandler (sys .stderr )
645+ console_handler .setLevel (level )
594646 console_handler .setFormatter (formatter )
595-
596- root_logger = logging .getLogger ()
597- root_logger .setLevel (level )
598- root_logger .handlers .clear ()
599647 root_logger .addHandler (console_handler )
600648
649+ if log_file is not None :
650+ file_level = (
651+ verbosity_to_level (log_file_verbosity )
652+ if log_file_verbosity is not None
653+ else level
654+ )
655+
656+ file_handler = logging .FileHandler (log_file , mode = "a" , encoding = "utf-8" )
657+ file_handler .setLevel (file_level )
658+ file_handler .setFormatter (formatter )
659+ root_logger .addHandler (file_handler )
660+
661+
662+ @app .route ("/update_ref/<path:path>" )
663+ def update_ref (path : str ):
664+ logger .debug (f"Updating reference for path: { path } " )
665+
666+ if not isinstance (path , str ):
667+ raise TypeError ("Path must be a string" )
668+
669+ src = Config .path_b / path
670+ dst = Config .path_a / path
671+
672+ if not src .exists ():
673+ return f"Source file does not exist: { src } " , 404
674+
675+ if src .is_file ():
676+ shutil .copy2 (src , dst )
677+ else :
678+ shutil .copytree (src , dst , dirs_exist_ok = True )
679+
680+ return "Reference updated" , 200
681+
601682
602683def main ():
603684 parser = argparse .ArgumentParser ()
604- parser .add_argument ("a " , type = Path , help = "Path to the first directory" )
605- parser .add_argument ("b " , type = Path , help = "Path to the second directory" )
685+ parser .add_argument ("ref " , type = Path , help = "Path to the reference directory" )
686+ parser .add_argument ("mon " , type = Path , help = "Path to the monitored directory" )
606687 parser .add_argument ("--driver" , choices = ["chrome" , "firefox" , "phantomjs" ])
607688 parser .add_argument ("--max-workers" , type = int , default = 1 )
608689 parser .add_argument ("--compare" , action = "store_true" )
@@ -614,16 +695,25 @@ def main():
614695 default = 0 ,
615696 help = "Increase verbosity (-v, -vv, -vvv)" ,
616697 )
698+ parser .add_argument (
699+ "--log-file" ,
700+ type = Path ,
701+ help = "Path to log file" ,
702+ )
703+ parser .add_argument (
704+ "--log-file-verbosity" , type = int , help = "Log file verbosity level"
705+ )
617706 args = parser .parse_args ()
618707
619- setup_logging (args .verbose )
708+ setup_logging (args .verbose , args . log_file , args . log_file_verbosity )
620709
621- Config .path_a = args .a
622- Config .path_b = args .b
710+ Config .path_a = args .ref
711+ Config .path_b = args .mon
623712 Config .driver = args .driver
624713 Config .browser = (
625714 get_browser (driver = args .driver ) if args .driver is not None else None
626715 )
716+ Config .log_file = args .log_file
627717
628718 if args .compare :
629719 Config .comparator = Comparator (max_workers = args .max_workers )
0 commit comments