htmlcmp 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/PKG-INFO +13 -1
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/README.md +12 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/pyproject.toml +1 -1
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/compare_output_server.py +160 -46
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/tidy_output.py +30 -8
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/PKG-INFO +13 -1
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/setup.cfg +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/__init__.py +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/common.py +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/compare_output.py +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp/html_render_diff.py +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/SOURCES.txt +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/dependency_links.txt +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/entry_points.txt +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/requires.txt +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/src/htmlcmp.egg-info/top_level.txt +0 -0
- {htmlcmp-1.1.0 → htmlcmp-1.2.0}/tests/test_html_render_diff.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlcmp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Compare HTML files by rendered output
|
|
5
5
|
Author: Andreas Stefl
|
|
6
6
|
Maintainer-email: Andreas Stefl <stefl.andreas@gmail.com>
|
|
@@ -53,3 +53,15 @@ docker run -ti \
|
|
|
53
53
|
```bash
|
|
54
54
|
docker build --tag odr_core_test test/docker
|
|
55
55
|
```
|
|
56
|
+
|
|
57
|
+
## Run locally
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
PYTHONPATH=$(pwd)/src:$PYTHONPATH python ./src/htmlcmp/compare_output_server.py \
|
|
61
|
+
/path/to/REFERENCE \
|
|
62
|
+
/path/to/MONITORED \
|
|
63
|
+
--compare \
|
|
64
|
+
--driver firefox \
|
|
65
|
+
--port 8000 \
|
|
66
|
+
-vv
|
|
67
|
+
```
|
|
@@ -35,3 +35,15 @@ docker run -ti \
|
|
|
35
35
|
```bash
|
|
36
36
|
docker build --tag odr_core_test test/docker
|
|
37
37
|
```
|
|
38
|
+
|
|
39
|
+
## Run locally
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PYTHONPATH=$(pwd)/src:$PYTHONPATH python ./src/htmlcmp/compare_output_server.py \
|
|
43
|
+
/path/to/REFERENCE \
|
|
44
|
+
/path/to/MONITORED \
|
|
45
|
+
--compare \
|
|
46
|
+
--driver firefox \
|
|
47
|
+
--port 8000 \
|
|
48
|
+
-vv
|
|
49
|
+
```
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import io
|
|
5
5
|
import sys
|
|
6
|
+
import shutil
|
|
6
7
|
import argparse
|
|
7
8
|
import logging
|
|
8
9
|
import threading
|
|
@@ -29,9 +30,10 @@ class Config:
|
|
|
29
30
|
comparator = None
|
|
30
31
|
browser = None
|
|
31
32
|
thread_local = threading.local()
|
|
33
|
+
log_file: Path = None
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
def result_symbol(result: str):
|
|
36
|
+
def result_symbol(result: str) -> str | None:
|
|
35
37
|
if not isinstance(result, str):
|
|
36
38
|
raise TypeError("Result must be of type str")
|
|
37
39
|
|
|
@@ -41,10 +43,10 @@ def result_symbol(result: str):
|
|
|
41
43
|
return "✔"
|
|
42
44
|
if result == "different":
|
|
43
45
|
return "❌"
|
|
44
|
-
return
|
|
46
|
+
return None
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def result_css(result: str):
|
|
49
|
+
def result_css(result: str) -> str | None:
|
|
48
50
|
if not isinstance(result, str):
|
|
49
51
|
raise TypeError("Result must be of type str")
|
|
50
52
|
|
|
@@ -53,8 +55,8 @@ def result_css(result: str):
|
|
|
53
55
|
if result == "same":
|
|
54
56
|
return "color:green;"
|
|
55
57
|
if result == "different":
|
|
56
|
-
return "color:
|
|
57
|
-
return
|
|
58
|
+
return "color:red;"
|
|
59
|
+
return None
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class Observer:
|
|
@@ -86,7 +88,7 @@ class Observer:
|
|
|
86
88
|
self._observer.schedule(Handler(Config.path_a), Config.path_a, recursive=True)
|
|
87
89
|
self._observer.schedule(Handler(Config.path_b), Config.path_b, recursive=True)
|
|
88
90
|
|
|
89
|
-
def start(self):
|
|
91
|
+
def start(self) -> None:
|
|
90
92
|
logger.info("Starting watchdog observer")
|
|
91
93
|
self._observer.start()
|
|
92
94
|
|
|
@@ -118,11 +120,11 @@ class Observer:
|
|
|
118
120
|
init_compare(Config.path_a, Config.path_b)
|
|
119
121
|
logger.info("Initial comparison submitted")
|
|
120
122
|
|
|
121
|
-
def stop(self):
|
|
123
|
+
def stop(self) -> None:
|
|
122
124
|
logger.info("Stopping watchdog observer")
|
|
123
125
|
self._observer.stop()
|
|
124
126
|
|
|
125
|
-
def join(self):
|
|
127
|
+
def join(self) -> None:
|
|
126
128
|
logger.info("Joining watchdog observer")
|
|
127
129
|
self._observer.join()
|
|
128
130
|
|
|
@@ -130,10 +132,11 @@ class Observer:
|
|
|
130
132
|
class Comparator:
|
|
131
133
|
def __init__(self, max_workers: int):
|
|
132
134
|
def initializer():
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
browser
|
|
136
|
-
|
|
135
|
+
if Config.driver is not None:
|
|
136
|
+
browser = getattr(Config.thread_local, "browser", None)
|
|
137
|
+
if browser is None:
|
|
138
|
+
browser = get_browser(driver=Config.driver)
|
|
139
|
+
Config.thread_local.browser = browser
|
|
137
140
|
|
|
138
141
|
logger.info(f"Creating comparator with {max_workers} workers")
|
|
139
142
|
|
|
@@ -143,12 +146,16 @@ class Comparator:
|
|
|
143
146
|
self._result = {}
|
|
144
147
|
self._future = {}
|
|
145
148
|
|
|
146
|
-
def submit(self, path: Path):
|
|
149
|
+
def submit(self, path: Path) -> None:
|
|
147
150
|
logger.debug(f"Submitting comparison for path: {path}")
|
|
148
151
|
|
|
149
152
|
if not isinstance(path, Path):
|
|
150
153
|
raise TypeError("Path must be of type Path")
|
|
151
154
|
|
|
155
|
+
if path.suffix.lower() in [".html", ".htm"] and Config.driver is None:
|
|
156
|
+
logger.debug(f"Skipping submission of HTML file without browser: {path}")
|
|
157
|
+
return
|
|
158
|
+
|
|
152
159
|
if path in self._future:
|
|
153
160
|
try:
|
|
154
161
|
self._future[path].cancel()
|
|
@@ -160,7 +167,7 @@ class Comparator:
|
|
|
160
167
|
self._result[path] = "pending"
|
|
161
168
|
self._future[path] = self._executor.submit(self.compare, path)
|
|
162
169
|
|
|
163
|
-
def compare(self, path: Path):
|
|
170
|
+
def compare(self, path: Path) -> None:
|
|
164
171
|
logger.debug(f"Comparing files for path: {path}")
|
|
165
172
|
|
|
166
173
|
if not isinstance(path, Path):
|
|
@@ -177,7 +184,7 @@ class Comparator:
|
|
|
177
184
|
self._result[path] = "same" if result else "different"
|
|
178
185
|
self._future.pop(path)
|
|
179
186
|
|
|
180
|
-
def result(self, path: Path):
|
|
187
|
+
def result(self, path: Path) -> str | None:
|
|
181
188
|
logger.debug(f"Getting comparison result for path: {path}")
|
|
182
189
|
|
|
183
190
|
if not isinstance(path, Path):
|
|
@@ -220,9 +227,13 @@ class Comparator:
|
|
|
220
227
|
|
|
221
228
|
return functools.reduce(
|
|
222
229
|
lambda a, b: (
|
|
223
|
-
|
|
224
|
-
if
|
|
225
|
-
else (
|
|
230
|
+
None
|
|
231
|
+
if None in (a, b)
|
|
232
|
+
else (
|
|
233
|
+
"pending"
|
|
234
|
+
if "pending" in (a, b)
|
|
235
|
+
else "different" if "different" in (a, b) else "same"
|
|
236
|
+
)
|
|
226
237
|
),
|
|
227
238
|
[self.result(path / name) for name in common]
|
|
228
239
|
+ [
|
|
@@ -235,20 +246,42 @@ class Comparator:
|
|
|
235
246
|
"same",
|
|
236
247
|
)
|
|
237
248
|
|
|
238
|
-
logger.
|
|
239
|
-
return
|
|
249
|
+
logger.debug(f"No comparison result for path: {path}")
|
|
250
|
+
return None
|
|
240
251
|
|
|
241
252
|
|
|
242
253
|
app = Flask("compare")
|
|
243
254
|
|
|
244
255
|
|
|
256
|
+
@app.route("/script.js")
|
|
257
|
+
def script_js():
|
|
258
|
+
logger.debug("Serving script.js")
|
|
259
|
+
|
|
260
|
+
return """
|
|
261
|
+
function updateRef(path) {
|
|
262
|
+
fetch(`/update_ref/${path}`)
|
|
263
|
+
.then(response => {
|
|
264
|
+
if (response.ok) {
|
|
265
|
+
alert(`Reference updated for ${path}`);
|
|
266
|
+
location.reload();
|
|
267
|
+
} else {
|
|
268
|
+
alert(`Failed to update reference for ${path}: ${response.statusText}`);
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
.catch(error => {
|
|
272
|
+
alert(`Error updating reference for ${path}: ${error}`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
|
|
245
278
|
@app.route("/")
|
|
246
279
|
def root():
|
|
247
280
|
logger.debug("Generating root directory listing")
|
|
248
281
|
|
|
249
282
|
current_entry_id = 0
|
|
250
283
|
|
|
251
|
-
def next_entry_id():
|
|
284
|
+
def next_entry_id() -> int:
|
|
252
285
|
nonlocal current_entry_id
|
|
253
286
|
entry_id = current_entry_id
|
|
254
287
|
current_entry_id += 1
|
|
@@ -284,7 +317,7 @@ def root():
|
|
|
284
317
|
result += "<td></td>"
|
|
285
318
|
|
|
286
319
|
if is_comparable:
|
|
287
|
-
main = f'<a href="/compare/{path}">{name}</a>'
|
|
320
|
+
main = f'<a href="/compare/{path}" target="_blank">{name}</a>'
|
|
288
321
|
else:
|
|
289
322
|
main = f"{name}"
|
|
290
323
|
if cmp_result is not None:
|
|
@@ -297,6 +330,10 @@ def root():
|
|
|
297
330
|
result += f'<td class="main" style="{style}">{main}</td>'
|
|
298
331
|
result += f"<td>{message}</td>"
|
|
299
332
|
|
|
333
|
+
result += (
|
|
334
|
+
f"<td><button onclick=\"updateRef('{path}')\">update ref</button></td>"
|
|
335
|
+
)
|
|
336
|
+
|
|
300
337
|
result += "</tr>"
|
|
301
338
|
|
|
302
339
|
return result
|
|
@@ -429,16 +466,24 @@ tr {
|
|
|
429
466
|
padding-left: calc(1.0rem * var(--depth));
|
|
430
467
|
}
|
|
431
468
|
</style>
|
|
469
|
+
<script src="/script.js"></script>
|
|
432
470
|
</head>
|
|
433
471
|
<body>
|
|
434
472
|
"""
|
|
435
473
|
|
|
436
474
|
result += "<p>"
|
|
437
475
|
result += "comparing<br>"
|
|
438
|
-
result += f"
|
|
439
|
-
result += f"
|
|
476
|
+
result += f"Reference: {Config.path_a}<br>"
|
|
477
|
+
result += f"Monitored: {Config.path_b}"
|
|
440
478
|
result += "</p>"
|
|
441
479
|
|
|
480
|
+
if Config.log_file is not None:
|
|
481
|
+
result += "<p>"
|
|
482
|
+
result += (
|
|
483
|
+
f'<a href="/logfile" target="_blank">View log file: {Config.log_file}</a>'
|
|
484
|
+
)
|
|
485
|
+
result += "</p>"
|
|
486
|
+
|
|
442
487
|
result += "<p>"
|
|
443
488
|
result += '<button onclick="toggleAll(true)">Expand All</button>'
|
|
444
489
|
result += '<button onclick="toggleAll(false)">Collapse All</button>'
|
|
@@ -446,7 +491,7 @@ tr {
|
|
|
446
491
|
|
|
447
492
|
result += "<table>"
|
|
448
493
|
result += "<thead>"
|
|
449
|
-
result += "<tr><td></td><td></td><td>Name</td><td>Message</td></tr>"
|
|
494
|
+
result += "<tr><td></td><td></td><td>Name</td><td>Message</td><td>Actions</td></tr>"
|
|
450
495
|
result += "</thead>"
|
|
451
496
|
result += "<tbody>"
|
|
452
497
|
result += generate_tree(Config.path_a, Config.path_b, "", None, 0)
|
|
@@ -471,6 +516,11 @@ function toggleChildren(parentId, show) {
|
|
|
471
516
|
document.querySelectorAll(`tr[data-parent-id="${parentId}"]`)
|
|
472
517
|
.forEach(child => {
|
|
473
518
|
child.hidden = !show;
|
|
519
|
+
if (!show) {
|
|
520
|
+
toggleChildren(child.dataset.entryId, false);
|
|
521
|
+
const toggle = child.querySelector(".toggle");
|
|
522
|
+
if (toggle) toggle.textContent = "▶";
|
|
523
|
+
}
|
|
474
524
|
});
|
|
475
525
|
}
|
|
476
526
|
|
|
@@ -493,6 +543,16 @@ function toggleAll(show) {
|
|
|
493
543
|
return result
|
|
494
544
|
|
|
495
545
|
|
|
546
|
+
@app.route("/logfile")
|
|
547
|
+
def logfile():
|
|
548
|
+
logger.debug("Serving log file")
|
|
549
|
+
|
|
550
|
+
if Config.log_file is None:
|
|
551
|
+
return "No log file configured", 404
|
|
552
|
+
|
|
553
|
+
return send_from_directory(Config.log_file.parent, Config.log_file.name)
|
|
554
|
+
|
|
555
|
+
|
|
496
556
|
@app.route("/compare/<path:path>")
|
|
497
557
|
def compare(path: str):
|
|
498
558
|
logger.debug(f"Generating comparison page for path: {path}")
|
|
@@ -506,18 +566,20 @@ def compare(path: str):
|
|
|
506
566
|
<style>
|
|
507
567
|
html,body {{height:100%;margin:0;}}
|
|
508
568
|
</style>
|
|
569
|
+
<script src="/script.js"></script>
|
|
509
570
|
</head>
|
|
510
571
|
<body style="display:flex;flex-flow:row;">
|
|
511
572
|
<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
|
|
512
|
-
<a href="/file/a/{path}">{Config.path_a / path}</a>
|
|
573
|
+
<a href="/file/a/{path}" target="_blank">{Config.path_a / path}</a>
|
|
513
574
|
<iframe id="a" src="/file/a/{path}" title="a" frameborder="0" align="left" style="flex:1;"></iframe>
|
|
514
575
|
</div>
|
|
515
576
|
<div style="display:flex;flex:0 0 50px;flex-flow:column;">
|
|
516
|
-
<a href="/image_diff/{path}">diff</a>
|
|
577
|
+
<a href="/image_diff/{path}" target="_blank">diff</a>
|
|
578
|
+
<button onclick="updateRef('{path}')">▶</button>
|
|
517
579
|
<img src="/image_diff/{path}" width="50" height="0" style="flex:1;">
|
|
518
580
|
</div>
|
|
519
581
|
<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
|
|
520
|
-
<a href="/file/b/{path}">{Config.path_b / path}</a>
|
|
582
|
+
<a href="/file/b/{path}" target="_blank">{Config.path_b / path}</a>
|
|
521
583
|
<iframe id="b" src="/file/b/{path}" title="b" frameborder="0" align="right" style="flex:1;"></iframe>
|
|
522
584
|
</div>
|
|
523
585
|
<script>
|
|
@@ -542,6 +604,9 @@ def image_diff(path: str):
|
|
|
542
604
|
if not isinstance(path, str):
|
|
543
605
|
raise TypeError("Path must be a string")
|
|
544
606
|
|
|
607
|
+
if Config.driver is None:
|
|
608
|
+
return "Image diff not available without browser driver", 404
|
|
609
|
+
|
|
545
610
|
diff, _ = html_render_diff(
|
|
546
611
|
Config.path_a / path,
|
|
547
612
|
Config.path_b / path,
|
|
@@ -566,15 +631,46 @@ def file(variant: str, path: str):
|
|
|
566
631
|
return send_from_directory(variant_root, path)
|
|
567
632
|
|
|
568
633
|
|
|
569
|
-
|
|
634
|
+
@app.route("/update_ref/<path:path>")
|
|
635
|
+
def update_ref(path: str):
|
|
636
|
+
logger.debug(f"Updating reference for path: {path}")
|
|
637
|
+
|
|
638
|
+
if not isinstance(path, str):
|
|
639
|
+
raise TypeError("Path must be a string")
|
|
640
|
+
|
|
641
|
+
src = Config.path_b / path
|
|
642
|
+
dst = Config.path_a / path
|
|
643
|
+
|
|
644
|
+
if not src.exists():
|
|
645
|
+
return f"Source file does not exist: {src}", 404
|
|
646
|
+
|
|
647
|
+
if src.is_file():
|
|
648
|
+
shutil.copy2(src, dst)
|
|
649
|
+
else:
|
|
650
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
651
|
+
|
|
652
|
+
return "Reference updated", 200
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def verbosity_to_level(verbosity: int) -> int:
|
|
570
656
|
if verbosity >= 3:
|
|
571
|
-
|
|
657
|
+
return logging.DEBUG
|
|
572
658
|
elif verbosity == 2:
|
|
573
|
-
|
|
659
|
+
return logging.INFO
|
|
574
660
|
elif verbosity == 1:
|
|
575
|
-
|
|
661
|
+
return logging.WARNING
|
|
576
662
|
else:
|
|
577
|
-
|
|
663
|
+
return logging.ERROR
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def setup_logging(
|
|
667
|
+
verbosity: int, log_file: Path = None, log_file_verbosity: int = None
|
|
668
|
+
) -> None:
|
|
669
|
+
level = verbosity_to_level(verbosity)
|
|
670
|
+
|
|
671
|
+
root_logger = logging.getLogger()
|
|
672
|
+
root_logger.setLevel(level)
|
|
673
|
+
root_logger.handlers.clear()
|
|
578
674
|
|
|
579
675
|
formatter = logging.Formatter(
|
|
580
676
|
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
@@ -582,21 +678,28 @@ def setup_logging(verbosity: int):
|
|
|
582
678
|
)
|
|
583
679
|
|
|
584
680
|
console_handler = logging.StreamHandler(sys.stderr)
|
|
681
|
+
console_handler.setLevel(level)
|
|
585
682
|
console_handler.setFormatter(formatter)
|
|
586
|
-
|
|
587
|
-
root_logger = logging.getLogger()
|
|
588
|
-
root_logger.setLevel(level)
|
|
589
|
-
root_logger.handlers.clear()
|
|
590
683
|
root_logger.addHandler(console_handler)
|
|
591
684
|
|
|
685
|
+
if log_file is not None:
|
|
686
|
+
file_level = (
|
|
687
|
+
verbosity_to_level(log_file_verbosity)
|
|
688
|
+
if log_file_verbosity is not None
|
|
689
|
+
else level
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
|
|
693
|
+
file_handler.setLevel(file_level)
|
|
694
|
+
file_handler.setFormatter(formatter)
|
|
695
|
+
root_logger.addHandler(file_handler)
|
|
696
|
+
|
|
592
697
|
|
|
593
698
|
def main():
|
|
594
699
|
parser = argparse.ArgumentParser()
|
|
595
|
-
parser.add_argument("
|
|
596
|
-
parser.add_argument("
|
|
597
|
-
parser.add_argument(
|
|
598
|
-
"--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
|
|
599
|
-
)
|
|
700
|
+
parser.add_argument("ref", type=Path, help="Path to the reference directory")
|
|
701
|
+
parser.add_argument("mon", type=Path, help="Path to the monitored directory")
|
|
702
|
+
parser.add_argument("--driver", choices=["chrome", "firefox", "phantomjs"])
|
|
600
703
|
parser.add_argument("--max-workers", type=int, default=1)
|
|
601
704
|
parser.add_argument("--compare", action="store_true")
|
|
602
705
|
parser.add_argument("--port", type=int, default=5000)
|
|
@@ -607,14 +710,25 @@ def main():
|
|
|
607
710
|
default=0,
|
|
608
711
|
help="Increase verbosity (-v, -vv, -vvv)",
|
|
609
712
|
)
|
|
713
|
+
parser.add_argument(
|
|
714
|
+
"--log-file",
|
|
715
|
+
type=Path,
|
|
716
|
+
help="Path to log file",
|
|
717
|
+
)
|
|
718
|
+
parser.add_argument(
|
|
719
|
+
"--log-file-verbosity", type=int, help="Log file verbosity level"
|
|
720
|
+
)
|
|
610
721
|
args = parser.parse_args()
|
|
611
722
|
|
|
612
|
-
setup_logging(args.verbose)
|
|
723
|
+
setup_logging(args.verbose, args.log_file, args.log_file_verbosity)
|
|
613
724
|
|
|
614
|
-
Config.path_a = args.
|
|
615
|
-
Config.path_b = args.
|
|
725
|
+
Config.path_a = args.ref
|
|
726
|
+
Config.path_b = args.mon
|
|
616
727
|
Config.driver = args.driver
|
|
617
|
-
Config.browser =
|
|
728
|
+
Config.browser = (
|
|
729
|
+
get_browser(driver=args.driver) if args.driver is not None else None
|
|
730
|
+
)
|
|
731
|
+
Config.log_file = args.log_file
|
|
618
732
|
|
|
619
733
|
if args.compare:
|
|
620
734
|
Config.comparator = Comparator(max_workers=args.max_workers)
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from htmlcmp.common import bcolors
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def tidy_json(path: Path) -> int:
|
|
13
|
+
def tidy_json(path: Path, verbose: bool = False) -> int:
|
|
14
14
|
if not isinstance(path, Path):
|
|
15
15
|
raise TypeError("path must be a Path object")
|
|
16
16
|
if not path.is_file():
|
|
@@ -21,10 +21,11 @@ def tidy_json(path: Path) -> int:
|
|
|
21
21
|
json.load(f)
|
|
22
22
|
return 0
|
|
23
23
|
except ValueError:
|
|
24
|
+
print(f"{bcolors.FAIL}Error: {path} is not a valid JSON file{bcolors.ENDC}")
|
|
24
25
|
return 1
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def tidy_html(path: Path, html_tidy_config: Path = None) -> int:
|
|
28
|
+
def tidy_html(path: Path, html_tidy_config: Path = None, verbose: bool = False) -> int:
|
|
28
29
|
if not isinstance(path, Path):
|
|
29
30
|
raise TypeError("path must be a Path object")
|
|
30
31
|
if not path.is_file():
|
|
@@ -41,6 +42,15 @@ def tidy_html(path: Path, html_tidy_config: Path = None) -> int:
|
|
|
41
42
|
result = subprocess.run(
|
|
42
43
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
43
44
|
)
|
|
45
|
+
if result.stdout:
|
|
46
|
+
if verbose and result.returncode == 0:
|
|
47
|
+
print(result.stdout)
|
|
48
|
+
elif verbose and result.returncode == 1:
|
|
49
|
+
print(f"{bcolors.WARNING}Warning: {path} has warnings{bcolors.ENDC}")
|
|
50
|
+
print(f"{bcolors.WARNING}{result.stdout}{bcolors.ENDC}")
|
|
51
|
+
elif verbose or result.returncode > 1:
|
|
52
|
+
print(f"{bcolors.FAIL}Error: {path} has errors{bcolors.ENDC}")
|
|
53
|
+
print(f"{bcolors.FAIL}{result.stdout}{bcolors.ENDC}")
|
|
44
54
|
if result.returncode == 1:
|
|
45
55
|
return 1
|
|
46
56
|
if result.returncode > 1:
|
|
@@ -48,16 +58,16 @@ def tidy_html(path: Path, html_tidy_config: Path = None) -> int:
|
|
|
48
58
|
return 0
|
|
49
59
|
|
|
50
60
|
|
|
51
|
-
def tidy_file(path: Path, html_tidy_config: Path = None) -> int:
|
|
61
|
+
def tidy_file(path: Path, html_tidy_config: Path = None, verbose: bool = False) -> int:
|
|
52
62
|
if not isinstance(path, Path):
|
|
53
63
|
raise TypeError("path must be a Path object")
|
|
54
64
|
if not path.is_file():
|
|
55
65
|
raise FileNotFoundError(f"{path} is not a file")
|
|
56
66
|
|
|
57
67
|
if path.suffix == ".json":
|
|
58
|
-
return tidy_json(path)
|
|
68
|
+
return tidy_json(path, verbose=verbose)
|
|
59
69
|
elif path.suffix == ".html":
|
|
60
|
-
return tidy_html(path, html_tidy_config=html_tidy_config)
|
|
70
|
+
return tidy_html(path, html_tidy_config=html_tidy_config, verbose=verbose)
|
|
61
71
|
|
|
62
72
|
|
|
63
73
|
def tidyable_file(path: Path) -> bool:
|
|
@@ -74,7 +84,11 @@ def tidyable_file(path: Path) -> bool:
|
|
|
74
84
|
|
|
75
85
|
|
|
76
86
|
def tidy_dir(
|
|
77
|
-
path: Path,
|
|
87
|
+
path: Path,
|
|
88
|
+
level: int = 0,
|
|
89
|
+
prefix: str = "",
|
|
90
|
+
html_tidy_config: Path = None,
|
|
91
|
+
verbose: bool = False,
|
|
78
92
|
) -> dict[str, list[Path]]:
|
|
79
93
|
if not isinstance(path, Path):
|
|
80
94
|
raise TypeError("path must be a Path object")
|
|
@@ -104,7 +118,7 @@ def tidy_dir(
|
|
|
104
118
|
|
|
105
119
|
for filename in [path.name for path in files]:
|
|
106
120
|
filepath = path / filename
|
|
107
|
-
tidy = tidy_file(filepath, html_tidy_config=html_tidy_config)
|
|
121
|
+
tidy = tidy_file(filepath, html_tidy_config=html_tidy_config, verbose=verbose)
|
|
108
122
|
if tidy == 0:
|
|
109
123
|
print(f"{prefix_file}{bcolors.OKGREEN}{filename} ✓{bcolors.ENDC}")
|
|
110
124
|
elif tidy == 1:
|
|
@@ -121,6 +135,7 @@ def tidy_dir(
|
|
|
121
135
|
level=level + 1,
|
|
122
136
|
prefix=prefix + "│ ",
|
|
123
137
|
html_tidy_config=html_tidy_config,
|
|
138
|
+
verbose=verbose,
|
|
124
139
|
)
|
|
125
140
|
result["warning"].extend(subresult["warning"])
|
|
126
141
|
result["error"].extend(subresult["error"])
|
|
@@ -134,9 +149,16 @@ def main():
|
|
|
134
149
|
parser.add_argument(
|
|
135
150
|
"--html-tidy-config", type=Path, help="Path to tidy config file"
|
|
136
151
|
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--verbose",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Print verbose output (warnings and errors)",
|
|
156
|
+
)
|
|
137
157
|
args = parser.parse_args()
|
|
138
158
|
|
|
139
|
-
result = tidy_dir(
|
|
159
|
+
result = tidy_dir(
|
|
160
|
+
args.path, html_tidy_config=args.html_tidy_config, verbose=args.verbose
|
|
161
|
+
)
|
|
140
162
|
if result["error"]:
|
|
141
163
|
return 1
|
|
142
164
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlcmp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Compare HTML files by rendered output
|
|
5
5
|
Author: Andreas Stefl
|
|
6
6
|
Maintainer-email: Andreas Stefl <stefl.andreas@gmail.com>
|
|
@@ -53,3 +53,15 @@ docker run -ti \
|
|
|
53
53
|
```bash
|
|
54
54
|
docker build --tag odr_core_test test/docker
|
|
55
55
|
```
|
|
56
|
+
|
|
57
|
+
## Run locally
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
PYTHONPATH=$(pwd)/src:$PYTHONPATH python ./src/htmlcmp/compare_output_server.py \
|
|
61
|
+
/path/to/REFERENCE \
|
|
62
|
+
/path/to/MONITORED \
|
|
63
|
+
--compare \
|
|
64
|
+
--driver firefox \
|
|
65
|
+
--port 8000 \
|
|
66
|
+
-vv
|
|
67
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|