htmlcmp 1.0.0__tar.gz → 1.0.1__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.0.0 → htmlcmp-1.0.1}/PKG-INFO +1 -1
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/pyproject.toml +1 -1
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/setup.py +1 -1
- htmlcmp-1.0.1/src/htmlcmp/__init__.py +0 -0
- htmlcmp-1.0.1/src/htmlcmp/common.py +10 -0
- htmlcmp-1.0.1/src/htmlcmp/compare_output.py +251 -0
- htmlcmp-1.0.1/src/htmlcmp/compare_output_server.py +310 -0
- htmlcmp-1.0.1/src/htmlcmp/html_render_diff.py +115 -0
- htmlcmp-1.0.1/src/htmlcmp/tidy_output.py +102 -0
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/src/htmlcmp.egg-info/PKG-INFO +1 -1
- htmlcmp-1.0.1/src/htmlcmp.egg-info/SOURCES.txt +14 -0
- htmlcmp-1.0.1/src/htmlcmp.egg-info/top_level.txt +1 -0
- htmlcmp-1.0.0/src/htmlcmp.egg-info/SOURCES.txt +0 -8
- htmlcmp-1.0.0/src/htmlcmp.egg-info/top_level.txt +0 -1
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/README.md +0 -0
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/setup.cfg +0 -0
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/src/htmlcmp.egg-info/dependency_links.txt +0 -0
- {htmlcmp-1.0.0 → htmlcmp-1.0.1}/src/htmlcmp.egg-info/entry_points.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import threading
|
|
9
|
+
import filecmp
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from html_render_diff import get_browser, html_render_diff
|
|
12
|
+
from common import bcolors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
thread_local = threading.local()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_json(path):
|
|
20
|
+
with open(path) as f:
|
|
21
|
+
return json.load(f)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compare_json(a, b):
|
|
25
|
+
json_a = json.dumps(parse_json(a), sort_keys=True)
|
|
26
|
+
json_b = json.dumps(parse_json(b), sort_keys=True)
|
|
27
|
+
return json_a == json_b
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compare_html(a, b, browser=None, diff_output=None):
|
|
31
|
+
if browser is None:
|
|
32
|
+
browser = get_browser()
|
|
33
|
+
diff, (image_a, image_b) = html_render_diff(a, b, browser=browser)
|
|
34
|
+
result = True if diff.getbbox() is None else False
|
|
35
|
+
if diff_output is not None and not result:
|
|
36
|
+
os.makedirs(diff_output, exist_ok=True)
|
|
37
|
+
image_a.save(os.path.join(diff_output, "a.png"))
|
|
38
|
+
image_b.save(os.path.join(diff_output, "b.png"))
|
|
39
|
+
diff.save(os.path.join(diff_output, "diff.png"))
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compare_files(a, b, **kwargs):
|
|
44
|
+
if filecmp.cmp(a, b):
|
|
45
|
+
return True
|
|
46
|
+
if a.endswith(".json"):
|
|
47
|
+
return compare_json(a, b)
|
|
48
|
+
if a.endswith(".html"):
|
|
49
|
+
return compare_html(a, b, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def comparable_file(path):
|
|
53
|
+
if path.endswith(".json"):
|
|
54
|
+
return True
|
|
55
|
+
if path.endswith(".html"):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def submit_compare_dirs(a, b, executor, diff_output=None, **kwargs):
|
|
61
|
+
results = {
|
|
62
|
+
"common_dirs": [],
|
|
63
|
+
"common_files": [],
|
|
64
|
+
"left_files_missing": [],
|
|
65
|
+
"right_files_missing": [],
|
|
66
|
+
"left_dirs_missing": [],
|
|
67
|
+
"right_dirs_missing": [],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
left = sorted(os.listdir(a))
|
|
71
|
+
right = sorted(os.listdir(b))
|
|
72
|
+
|
|
73
|
+
left_files = sorted(
|
|
74
|
+
[
|
|
75
|
+
name
|
|
76
|
+
for name in left
|
|
77
|
+
if os.path.isfile(os.path.join(a, name))
|
|
78
|
+
and comparable_file(os.path.join(a, name))
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
right_files = sorted(
|
|
82
|
+
[
|
|
83
|
+
name
|
|
84
|
+
for name in right
|
|
85
|
+
if os.path.isfile(os.path.join(b, name))
|
|
86
|
+
and comparable_file(os.path.join(b, name))
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
common_files = [name for name in left_files if name in right_files]
|
|
90
|
+
|
|
91
|
+
def compare(path_a, path_b, diff_output):
|
|
92
|
+
browser = getattr(Config.thread_local, "browser", None)
|
|
93
|
+
return compare_files(path_a, path_b, browser=browser, diff_output=diff_output)
|
|
94
|
+
|
|
95
|
+
for name in common_files:
|
|
96
|
+
future = executor.submit(
|
|
97
|
+
compare,
|
|
98
|
+
os.path.join(a, name),
|
|
99
|
+
os.path.join(b, name),
|
|
100
|
+
diff_output=(
|
|
101
|
+
None if diff_output is None else os.path.join(diff_output, name)
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
results["common_files"].append((name, future))
|
|
105
|
+
|
|
106
|
+
results["left_files_missing"] = [
|
|
107
|
+
name for name in right_files if name not in left_files
|
|
108
|
+
]
|
|
109
|
+
results["right_files_missing"] = [
|
|
110
|
+
name for name in left_files if name not in right_files
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
left_dirs = sorted([name for name in left if os.path.isdir(os.path.join(a, name))])
|
|
114
|
+
right_dirs = sorted(
|
|
115
|
+
[name for name in right if os.path.isdir(os.path.join(b, name))]
|
|
116
|
+
)
|
|
117
|
+
common_dirs = [path for path in left_dirs if path in right_dirs]
|
|
118
|
+
|
|
119
|
+
for name in common_dirs:
|
|
120
|
+
sub_results = submit_compare_dirs(
|
|
121
|
+
os.path.join(a, name),
|
|
122
|
+
os.path.join(b, name),
|
|
123
|
+
executor=executor,
|
|
124
|
+
diff_output=(
|
|
125
|
+
None if diff_output is None else os.path.join(diff_output, name)
|
|
126
|
+
),
|
|
127
|
+
**kwargs,
|
|
128
|
+
)
|
|
129
|
+
results["common_dirs"].append((name, sub_results))
|
|
130
|
+
|
|
131
|
+
results["left_dirs_missing"] = [
|
|
132
|
+
name for name in right_dirs if name not in left_dirs
|
|
133
|
+
]
|
|
134
|
+
results["right_dirs_missing"] = [
|
|
135
|
+
name for name in left_dirs if name not in right_dirs
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return results
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def print_results(results, a, b, level=0, prefix=""):
|
|
142
|
+
prefix_file = prefix + "├── "
|
|
143
|
+
if level == 0:
|
|
144
|
+
print(f"compare dir {a} with {b}")
|
|
145
|
+
|
|
146
|
+
result = {
|
|
147
|
+
"left_files_missing": [],
|
|
148
|
+
"right_files_missing": [],
|
|
149
|
+
"left_dirs_missing": [],
|
|
150
|
+
"right_dirs_missing": [],
|
|
151
|
+
"files_different": [],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
left_files_missing = " ".join(results["left_files_missing"])
|
|
155
|
+
if left_files_missing:
|
|
156
|
+
print(
|
|
157
|
+
f"{prefix_file}{bcolors.FAIL}missing files left: {left_files_missing} ✘{bcolors.ENDC}"
|
|
158
|
+
)
|
|
159
|
+
result["left_files_missing"].extend(
|
|
160
|
+
[os.path.join(a, name) for name in results["left_files_missing"]]
|
|
161
|
+
)
|
|
162
|
+
right_files_missing = " ".join(results["right_files_missing"])
|
|
163
|
+
if right_files_missing:
|
|
164
|
+
print(
|
|
165
|
+
f"{prefix_file}{bcolors.FAIL}missing files right: {right_files_missing} ✘{bcolors.ENDC}"
|
|
166
|
+
)
|
|
167
|
+
result["right_files_missing"].extend(
|
|
168
|
+
[os.path.join(a, name) for name in results["right_files_missing"]]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for name, future in results["common_files"]:
|
|
172
|
+
cmp = future.result()
|
|
173
|
+
if cmp:
|
|
174
|
+
print(f"{prefix_file}{bcolors.OKGREEN}{name} ✓{bcolors.ENDC}")
|
|
175
|
+
else:
|
|
176
|
+
print(f"{prefix_file}{bcolors.FAIL}{name} ✘{bcolors.ENDC}")
|
|
177
|
+
result["files_different"].append(os.path.join(a, name))
|
|
178
|
+
|
|
179
|
+
left_dirs_missing = " ".join(results["left_dirs_missing"])
|
|
180
|
+
if left_dirs_missing:
|
|
181
|
+
print(
|
|
182
|
+
f"{prefix_file}{bcolors.FAIL}missing dirs left: {left_dirs_missing} ✘{bcolors.ENDC}"
|
|
183
|
+
)
|
|
184
|
+
result["left_dirs_missing"].extend(
|
|
185
|
+
[os.path.join(a, name) for name in results["left_dirs_missing"]]
|
|
186
|
+
)
|
|
187
|
+
right_dirs_missing = " ".join(results["right_dirs_missing"])
|
|
188
|
+
if right_dirs_missing:
|
|
189
|
+
print(
|
|
190
|
+
f"{prefix_file}{bcolors.FAIL}missing dirs right: {right_dirs_missing} ✘{bcolors.ENDC}"
|
|
191
|
+
)
|
|
192
|
+
result["right_dirs_missing"].extend(
|
|
193
|
+
[os.path.join(a, name) for name in results["right_dirs_missing"]]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
for name, sub_results in results["common_dirs"]:
|
|
197
|
+
print(prefix + "├── " + name)
|
|
198
|
+
sub_result = print_results(
|
|
199
|
+
sub_results,
|
|
200
|
+
os.path.join(a, name),
|
|
201
|
+
os.path.join(b, name),
|
|
202
|
+
level=level + 1,
|
|
203
|
+
prefix=prefix + "│ ",
|
|
204
|
+
)
|
|
205
|
+
result["left_files_missing"].extend(sub_result["left_files_missing"])
|
|
206
|
+
result["right_files_missing"].extend(sub_result["right_files_missing"])
|
|
207
|
+
result["left_dirs_missing"].extend(sub_result["left_dirs_missing"])
|
|
208
|
+
result["right_dirs_missing"].extend(sub_result["right_dirs_missing"])
|
|
209
|
+
result["files_different"].extend(sub_result["files_different"])
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
parser = argparse.ArgumentParser()
|
|
216
|
+
parser.add_argument("a")
|
|
217
|
+
parser.add_argument("b")
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument("--diff-output")
|
|
222
|
+
parser.add_argument("--max-workers", type=int, default=1)
|
|
223
|
+
args = parser.parse_args()
|
|
224
|
+
|
|
225
|
+
def initializer():
|
|
226
|
+
browser = getattr(Config.thread_local, "browser", None)
|
|
227
|
+
if browser is None:
|
|
228
|
+
browser = get_browser(driver=args.driver)
|
|
229
|
+
Config.thread_local.browser = browser
|
|
230
|
+
|
|
231
|
+
executor = ThreadPoolExecutor(max_workers=args.max_workers, initializer=initializer)
|
|
232
|
+
|
|
233
|
+
results = submit_compare_dirs(
|
|
234
|
+
args.a, args.b, executor=executor, diff_output=args.diff_output
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
result = print_results(results, args.a, args.b)
|
|
238
|
+
if (
|
|
239
|
+
result["left_files_missing"]
|
|
240
|
+
or result["right_files_missing"]
|
|
241
|
+
or result["left_dirs_missing"]
|
|
242
|
+
or result["right_dirs_missing"]
|
|
243
|
+
or result["files_different"]
|
|
244
|
+
):
|
|
245
|
+
return 1
|
|
246
|
+
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
sys.exit(main())
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import threading
|
|
8
|
+
import io
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
from html_render_diff import get_browser, html_render_diff
|
|
11
|
+
from compare_output import comparable_file, compare_files
|
|
12
|
+
from flask import Flask, send_from_directory, send_file
|
|
13
|
+
import watchdog.observers
|
|
14
|
+
import watchdog.events
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Config:
|
|
18
|
+
path_a = None
|
|
19
|
+
path_b = None
|
|
20
|
+
driver = None
|
|
21
|
+
observer = None
|
|
22
|
+
comparator = None
|
|
23
|
+
browser = None
|
|
24
|
+
thread_local = threading.local()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Observer:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
class Handler(watchdog.events.FileSystemEventHandler):
|
|
30
|
+
def __init__(self, path):
|
|
31
|
+
self._path = path
|
|
32
|
+
|
|
33
|
+
def dispatch(self, event):
|
|
34
|
+
if event.event_type in ["opened"]:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if os.path.isfile(event.src_path):
|
|
38
|
+
Config.comparator.submit(
|
|
39
|
+
os.path.relpath(event.src_path, self._path)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self._observer = watchdog.observers.Observer()
|
|
43
|
+
self._observer.schedule(Handler(Config.path_a), Config.path_a, recursive=True)
|
|
44
|
+
self._observer.schedule(Handler(Config.path_b), Config.path_b, recursive=True)
|
|
45
|
+
|
|
46
|
+
def start(self):
|
|
47
|
+
self._observer.start()
|
|
48
|
+
|
|
49
|
+
def init_compare(a, b):
|
|
50
|
+
common_path = os.path.relpath(a, Config.path_a)
|
|
51
|
+
|
|
52
|
+
left = sorted(os.listdir(a))
|
|
53
|
+
right = sorted(os.listdir(b))
|
|
54
|
+
|
|
55
|
+
common = [name for name in left if name in right]
|
|
56
|
+
|
|
57
|
+
for name in common:
|
|
58
|
+
if os.path.isfile(os.path.join(a, name)) and comparable_file(
|
|
59
|
+
os.path.join(a, name)
|
|
60
|
+
):
|
|
61
|
+
Config.comparator.submit(os.path.join(common_path, name))
|
|
62
|
+
elif os.path.isdir(os.path.join(a, name)):
|
|
63
|
+
init_compare(os.path.join(a, name), os.path.join(b, name))
|
|
64
|
+
|
|
65
|
+
init_compare(Config.path_a, Config.path_b)
|
|
66
|
+
|
|
67
|
+
def stop(self):
|
|
68
|
+
self._observer.stop()
|
|
69
|
+
|
|
70
|
+
def join(self):
|
|
71
|
+
self._observer.join()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Comparator:
|
|
75
|
+
def __init__(self, max_workers):
|
|
76
|
+
def initializer():
|
|
77
|
+
browser = getattr(Config.thread_local, "browser", None)
|
|
78
|
+
if browser is None:
|
|
79
|
+
browser = get_browser(driver=Config.driver)
|
|
80
|
+
Config.thread_local.browser = browser
|
|
81
|
+
|
|
82
|
+
self._executor = ThreadPoolExecutor(
|
|
83
|
+
max_workers=max_workers, initializer=initializer
|
|
84
|
+
)
|
|
85
|
+
self._result = {}
|
|
86
|
+
self._future = {}
|
|
87
|
+
|
|
88
|
+
def submit(self, path):
|
|
89
|
+
if path in self._future:
|
|
90
|
+
try:
|
|
91
|
+
self._future[path].cancel()
|
|
92
|
+
self._future[path].result()
|
|
93
|
+
self._future.pop(path)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
self._result[path] = "pending"
|
|
98
|
+
self._future[path] = self._executor.submit(self.compare, path)
|
|
99
|
+
|
|
100
|
+
def compare(self, path):
|
|
101
|
+
browser = getattr(Config.thread_local, "browser", None)
|
|
102
|
+
result = compare_files(
|
|
103
|
+
os.path.join(Config.path_a, path),
|
|
104
|
+
os.path.join(Config.path_b, path),
|
|
105
|
+
browser=browser,
|
|
106
|
+
)
|
|
107
|
+
self._result[path] = "same" if result else "different"
|
|
108
|
+
self._future.pop(path)
|
|
109
|
+
|
|
110
|
+
def result(self, path):
|
|
111
|
+
if path in self._result:
|
|
112
|
+
return self._result[path]
|
|
113
|
+
return "unknown"
|
|
114
|
+
|
|
115
|
+
def result_symbol(self, path):
|
|
116
|
+
result = self.result(path)
|
|
117
|
+
if result == "pending":
|
|
118
|
+
return "🔄"
|
|
119
|
+
if result == "same":
|
|
120
|
+
return "✔"
|
|
121
|
+
if result == "different":
|
|
122
|
+
return "❌"
|
|
123
|
+
return "⛔"
|
|
124
|
+
|
|
125
|
+
def result_css(self, path):
|
|
126
|
+
result = self.result(path)
|
|
127
|
+
if result == "pending":
|
|
128
|
+
return "color:blue;"
|
|
129
|
+
if result == "same":
|
|
130
|
+
return "color:green;"
|
|
131
|
+
if result == "different":
|
|
132
|
+
return "color:orange;"
|
|
133
|
+
return "color:red;"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
app = Flask("compare")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.route("/")
|
|
140
|
+
def root():
|
|
141
|
+
def print_tree(a, b):
|
|
142
|
+
common_path = os.path.relpath(a, Config.path_a)
|
|
143
|
+
|
|
144
|
+
left = sorted(os.listdir(a))
|
|
145
|
+
right = sorted(os.listdir(b))
|
|
146
|
+
|
|
147
|
+
left_files = sorted(
|
|
148
|
+
[
|
|
149
|
+
name
|
|
150
|
+
for name in left
|
|
151
|
+
if os.path.isfile(os.path.join(a, name))
|
|
152
|
+
and comparable_file(os.path.join(a, name))
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
right_files = sorted(
|
|
156
|
+
[
|
|
157
|
+
name
|
|
158
|
+
for name in right
|
|
159
|
+
if os.path.isfile(os.path.join(b, name))
|
|
160
|
+
and comparable_file(os.path.join(b, name))
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
left_dirs = sorted(
|
|
165
|
+
[name for name in left if os.path.isdir(os.path.join(a, name))]
|
|
166
|
+
)
|
|
167
|
+
right_dirs = sorted(
|
|
168
|
+
[name for name in right if os.path.isdir(os.path.join(b, name))]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
common_files = [name for name in left_files if name in right_files]
|
|
172
|
+
common_dirs = [name for name in left_dirs if name in right_dirs]
|
|
173
|
+
|
|
174
|
+
result = "<ul>"
|
|
175
|
+
|
|
176
|
+
left_files_missing = " ".join(
|
|
177
|
+
[name for name in right_files if name not in left_files]
|
|
178
|
+
)
|
|
179
|
+
right_files_missing = " ".join(
|
|
180
|
+
[name for name in left_files if name not in right_files]
|
|
181
|
+
)
|
|
182
|
+
left_dirs_missing = " ".join(
|
|
183
|
+
[name for name in right_dirs if name not in left_dirs]
|
|
184
|
+
)
|
|
185
|
+
right_dirs_missing = " ".join(
|
|
186
|
+
[name for name in left_dirs if name not in right_dirs]
|
|
187
|
+
)
|
|
188
|
+
if left_files_missing:
|
|
189
|
+
result += f"<li><b>A files missing: {left_files_missing}</b></li>"
|
|
190
|
+
if right_files_missing:
|
|
191
|
+
result += f"<li><b>B files missing: {right_files_missing}</b></li>"
|
|
192
|
+
if left_dirs_missing:
|
|
193
|
+
result += f"<li><b>A dirs missing: {left_dirs_missing}</b></li>"
|
|
194
|
+
if right_dirs_missing:
|
|
195
|
+
result += f"<li><b>B dirs missing: {right_dirs_missing}</b></li>"
|
|
196
|
+
|
|
197
|
+
for name in common_files:
|
|
198
|
+
if Config.comparator is None:
|
|
199
|
+
result += f'<li><a href="/compare/{os.path.join(common_path, name)}">{name}</a></li>'
|
|
200
|
+
else:
|
|
201
|
+
symbol = Config.comparator.result_symbol(
|
|
202
|
+
os.path.join(common_path, name)
|
|
203
|
+
)
|
|
204
|
+
css = Config.comparator.result_css(os.path.join(common_path, name))
|
|
205
|
+
result += f'<li style="{css}"><a style="{css}" href="/compare/{os.path.join(common_path, name)}">{name}</a> {symbol}</li>'
|
|
206
|
+
|
|
207
|
+
for name in common_dirs:
|
|
208
|
+
result += f"<li>{name}"
|
|
209
|
+
result += print_tree(os.path.join(a, name), os.path.join(b, name))
|
|
210
|
+
result += "</li>"
|
|
211
|
+
|
|
212
|
+
result += "</ul>"
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
result = ""
|
|
216
|
+
result += f"<p>compare A {Config.path_a} vs B {Config.path_b}</p>"
|
|
217
|
+
result += print_tree(Config.path_a, Config.path_b)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.route("/compare/<path:path>")
|
|
222
|
+
def compare(path):
|
|
223
|
+
return f"""<!DOCTYPE html>
|
|
224
|
+
<html>
|
|
225
|
+
<head>
|
|
226
|
+
<style>
|
|
227
|
+
html,body {{height:100%;margin:0;}}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body style="display:flex;flex-flow:row;">
|
|
231
|
+
<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
|
|
232
|
+
<a href="/file/a/{path}">{os.path.join(Config.path_a, path)}</a>
|
|
233
|
+
<iframe id="a" src="/file/a/{path}" title="a" frameborder="0" align="left" style="flex:1;"></iframe>
|
|
234
|
+
</div>
|
|
235
|
+
<div style="display:flex;flex:0 0 50px;flex-flow:column;">
|
|
236
|
+
<a href="/image_diff/{path}">diff</a>
|
|
237
|
+
<img src="/image_diff/{path}" width="50" height="0" style="flex:1;">
|
|
238
|
+
</div>
|
|
239
|
+
<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
|
|
240
|
+
<a href="/file/b/{path}">{os.path.join(Config.path_b, path)}</a>
|
|
241
|
+
<iframe id="b" src="/file/b/{path}" title="b" frameborder="0" align="right" style="flex:1;"></iframe>
|
|
242
|
+
</div>
|
|
243
|
+
<script>
|
|
244
|
+
var iframe_a = document.getElementById('a');
|
|
245
|
+
var iframe_b = document.getElementById('b');
|
|
246
|
+
iframe_a.contentWindow.addEventListener('scroll', function(event) {{
|
|
247
|
+
iframe_b.contentWindow.scrollTo(iframe_a.contentWindow.scrollX, iframe_a.contentWindow.scrollY);
|
|
248
|
+
}});
|
|
249
|
+
iframe_b.contentWindow.addEventListener('scroll', function(event) {{
|
|
250
|
+
iframe_a.contentWindow.scrollTo(iframe_b.contentWindow.scrollX, iframe_b.contentWindow.scrollY);
|
|
251
|
+
}});
|
|
252
|
+
</script>
|
|
253
|
+
</body>
|
|
254
|
+
</html>
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.route("/image_diff/<path:path>")
|
|
259
|
+
def image_diff(path):
|
|
260
|
+
diff, _ = html_render_diff(
|
|
261
|
+
os.path.join(Config.path_a, path),
|
|
262
|
+
os.path.join(Config.path_b, path),
|
|
263
|
+
Config.browser,
|
|
264
|
+
)
|
|
265
|
+
tmp = io.BytesIO()
|
|
266
|
+
diff.save(tmp, "JPEG", quality=70)
|
|
267
|
+
tmp.seek(0)
|
|
268
|
+
return send_file(tmp, mimetype="image/jpeg")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.route("/file/<variant>/<path:path>")
|
|
272
|
+
def file(variant, path):
|
|
273
|
+
variant_root = Config.path_a if variant == "a" else Config.path_b
|
|
274
|
+
return send_from_directory(variant_root, path)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main():
|
|
278
|
+
parser = argparse.ArgumentParser()
|
|
279
|
+
parser.add_argument("a")
|
|
280
|
+
parser.add_argument("b")
|
|
281
|
+
parser.add_argument(
|
|
282
|
+
"--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
|
|
283
|
+
)
|
|
284
|
+
parser.add_argument("--max-workers", type=int, default=1)
|
|
285
|
+
parser.add_argument("--compare", action="store_true")
|
|
286
|
+
parser.add_argument("--port", type=int, default=5000)
|
|
287
|
+
args = parser.parse_args()
|
|
288
|
+
|
|
289
|
+
Config.path_a = args.a
|
|
290
|
+
Config.path_b = args.b
|
|
291
|
+
Config.driver = args.driver
|
|
292
|
+
Config.browser = get_browser(driver=args.driver)
|
|
293
|
+
|
|
294
|
+
if args.compare:
|
|
295
|
+
Config.comparator = Comparator(max_workers=args.max_workers)
|
|
296
|
+
|
|
297
|
+
Config.observer = Observer()
|
|
298
|
+
Config.observer.start()
|
|
299
|
+
|
|
300
|
+
app.run(host="0.0.0.0", port=args.port)
|
|
301
|
+
|
|
302
|
+
if args.compare:
|
|
303
|
+
Config.observer.stop()
|
|
304
|
+
Config.observer.join()
|
|
305
|
+
|
|
306
|
+
return 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
sys.exit(main())
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import io
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from PIL import Image, ImageChops
|
|
11
|
+
from selenium import webdriver
|
|
12
|
+
from selenium.webdriver.common.by import By
|
|
13
|
+
from selenium.webdriver.support import expected_conditions
|
|
14
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
15
|
+
import pathlib
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_url(something):
|
|
19
|
+
if os.path.isfile(something):
|
|
20
|
+
return pathlib.Path(os.path.abspath(something)).as_uri()
|
|
21
|
+
return something
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def screenshot(browser, url):
|
|
25
|
+
browser.get(url)
|
|
26
|
+
|
|
27
|
+
target_find_by = By.TAG_NAME
|
|
28
|
+
target = "body"
|
|
29
|
+
loaded_page_settling_time = 0
|
|
30
|
+
|
|
31
|
+
# Selenium doesn't like when we try to screenshot <body> element of documents generated by pdf2htmlEX
|
|
32
|
+
if "poppler" in url:
|
|
33
|
+
target_find_by = By.ID
|
|
34
|
+
target = "page-container"
|
|
35
|
+
loaded_page_settling_time = 1
|
|
36
|
+
|
|
37
|
+
web_driver_wait = WebDriverWait(browser, 5)
|
|
38
|
+
web_driver_wait.until(
|
|
39
|
+
expected_conditions.presence_of_element_located((target_find_by, target))
|
|
40
|
+
)
|
|
41
|
+
web_driver_wait.until(
|
|
42
|
+
lambda driver: driver.execute_script("return document.readyState") == "complete"
|
|
43
|
+
)
|
|
44
|
+
if loaded_page_settling_time != 0:
|
|
45
|
+
time.sleep(loaded_page_settling_time)
|
|
46
|
+
|
|
47
|
+
target_element = browser.find_element(target_find_by, target)
|
|
48
|
+
|
|
49
|
+
png = target_element.screenshot_as_png
|
|
50
|
+
return Image.open(io.BytesIO(png))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_browser(driver="firefox", max_width=1000, max_height=10000):
|
|
54
|
+
if driver == "phantomjs":
|
|
55
|
+
browser = webdriver.PhantomJS()
|
|
56
|
+
elif driver == "firefox":
|
|
57
|
+
options = webdriver.FirefoxOptions()
|
|
58
|
+
options.add_argument("--headless")
|
|
59
|
+
browser = webdriver.Firefox(options=options)
|
|
60
|
+
else: # chrome or unknown
|
|
61
|
+
options = webdriver.ChromeOptions()
|
|
62
|
+
options.add_argument("--headless=new")
|
|
63
|
+
browser = webdriver.Chrome(options=options)
|
|
64
|
+
browser.set_window_size(max_width, max_height)
|
|
65
|
+
return browser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def html_render_diff(a, b, browser):
|
|
69
|
+
image_a = screenshot(browser, to_url(a))
|
|
70
|
+
image_b = screenshot(browser, to_url(b))
|
|
71
|
+
|
|
72
|
+
image_a = image_a.convert("RGB")
|
|
73
|
+
image_b = image_b.convert("RGB")
|
|
74
|
+
diff = ImageChops.difference(image_a, image_b)
|
|
75
|
+
return diff, (image_a, image_b)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
parser = argparse.ArgumentParser()
|
|
80
|
+
parser.add_argument("a")
|
|
81
|
+
parser.add_argument("b")
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument("--max-width", default=1000)
|
|
86
|
+
parser.add_argument("--max-height", default=10000)
|
|
87
|
+
args = parser.parse_args()
|
|
88
|
+
|
|
89
|
+
browser = get_browser(args.driver, args.max_width, args.max_height)
|
|
90
|
+
diff, _ = html_render_diff(args.a, args.b, browser)
|
|
91
|
+
browser.quit()
|
|
92
|
+
bounding_box = diff.getbbox()
|
|
93
|
+
|
|
94
|
+
if bounding_box:
|
|
95
|
+
print("images are different")
|
|
96
|
+
print("first error at %f%%" % (1e2 * bounding_box[1] / diff.height))
|
|
97
|
+
print(
|
|
98
|
+
"bounding box %f%%"
|
|
99
|
+
% (
|
|
100
|
+
1e2
|
|
101
|
+
* (
|
|
102
|
+
(bounding_box[2] - bounding_box[0])
|
|
103
|
+
* (bounding_box[3] - bounding_box[1])
|
|
104
|
+
)
|
|
105
|
+
/ (diff.width * diff.height)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
print("images are the same")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
sys.exit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import shlex
|
|
10
|
+
from common import bcolors
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def tidy_json(path):
|
|
14
|
+
try:
|
|
15
|
+
with open(path, "r") as f:
|
|
16
|
+
json.load(f)
|
|
17
|
+
return 0
|
|
18
|
+
except ValueError:
|
|
19
|
+
return 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def tidy_html(path):
|
|
23
|
+
config_path = os.path.join(
|
|
24
|
+
os.path.dirname(os.path.realpath(__file__)), ".html-tidy"
|
|
25
|
+
)
|
|
26
|
+
cmd = shlex.split(f'tidy -config "{config_path}" -q "{path}"')
|
|
27
|
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
28
|
+
if result.returncode == 1:
|
|
29
|
+
return 1
|
|
30
|
+
if result.returncode > 1:
|
|
31
|
+
return 2
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tidy_file(path):
|
|
36
|
+
if path.endswith(".json"):
|
|
37
|
+
return tidy_json(path)
|
|
38
|
+
elif path.endswith(".html"):
|
|
39
|
+
return tidy_html(path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def tidyable_file(path):
|
|
43
|
+
if path.endswith(".json"):
|
|
44
|
+
return True
|
|
45
|
+
if path.endswith(".html"):
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def tidy_dir(path, level=0, prefix=""):
|
|
51
|
+
prefix_file = prefix + "├── "
|
|
52
|
+
if level == 0:
|
|
53
|
+
print(f"tidy dir {path}")
|
|
54
|
+
|
|
55
|
+
result = {
|
|
56
|
+
"warning": [],
|
|
57
|
+
"error": [],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
items = [os.path.join(path, name) for name in os.listdir(path)]
|
|
61
|
+
files = sorted(
|
|
62
|
+
[path for path in items if os.path.isfile(path) and tidyable_file(path)]
|
|
63
|
+
)
|
|
64
|
+
dirs = sorted([path for path in items if os.path.isdir(path)])
|
|
65
|
+
|
|
66
|
+
for filename in [os.path.basename(path) for path in files]:
|
|
67
|
+
filepath = os.path.join(path, filename)
|
|
68
|
+
tidy = tidy_file(filepath)
|
|
69
|
+
if tidy == 0:
|
|
70
|
+
print(f"{prefix_file}{bcolors.OKGREEN}{filename} ✓{bcolors.ENDC}")
|
|
71
|
+
elif tidy == 1:
|
|
72
|
+
print(f"{prefix_file}{bcolors.WARNING}{filename} ✓{bcolors.ENDC}")
|
|
73
|
+
result["warning"].append(filepath)
|
|
74
|
+
elif tidy > 1:
|
|
75
|
+
print(f"{prefix_file}{bcolors.FAIL}{filename} ✘{bcolors.ENDC}")
|
|
76
|
+
result["error"].append(filepath)
|
|
77
|
+
|
|
78
|
+
for dirname in [os.path.basename(path) for path in dirs]:
|
|
79
|
+
print(prefix + "├── " + dirname)
|
|
80
|
+
subresult = tidy_dir(
|
|
81
|
+
os.path.join(path, dirname), level=level + 1, prefix=prefix + "│ "
|
|
82
|
+
)
|
|
83
|
+
result["warning"].extend(subresult["warning"])
|
|
84
|
+
result["error"].extend(subresult["error"])
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
parser = argparse.ArgumentParser()
|
|
91
|
+
parser.add_argument("a")
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
result = tidy_dir(args.a)
|
|
95
|
+
if result["error"]:
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
sys.exit(main())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/htmlcmp/__init__.py
|
|
5
|
+
src/htmlcmp/common.py
|
|
6
|
+
src/htmlcmp/compare_output.py
|
|
7
|
+
src/htmlcmp/compare_output_server.py
|
|
8
|
+
src/htmlcmp/html_render_diff.py
|
|
9
|
+
src/htmlcmp/tidy_output.py
|
|
10
|
+
src/htmlcmp.egg-info/PKG-INFO
|
|
11
|
+
src/htmlcmp.egg-info/SOURCES.txt
|
|
12
|
+
src/htmlcmp.egg-info/dependency_links.txt
|
|
13
|
+
src/htmlcmp.egg-info/entry_points.txt
|
|
14
|
+
src/htmlcmp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
htmlcmp
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|