Skip to content

Commit b93b2dc

Browse files
bjlittlemarqh
authored andcommitted
Refactor idiff for imagerepo.json capability. (SciTools#2166)
* Refactor idiff for imagerepo.json capability.
1 parent 8b1e2f3 commit b93b2dc

File tree

4 files changed

+157
-38
lines changed

4 files changed

+157
-38
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ lib/iris/std_names.py
3333
lib/iris/tests/result_image_comparison/
3434
iris_image_test_output/
3535

36+
# Iris test lock file
37+
lib/iris/tests/results/imagerepo.lock
38+
3639
# Pydev/Eclipse files
3740
.project
3841
.pydevproject

conda-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nose
2020
pep8=1.5.7
2121
sphinx
2222
iris_sample_data
23+
filelock
2324

2425
# Optional iris dependencies
2526
nc_time_axis

lib/iris/tests/idiff.py

+152-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# (C) British Crown Copyright 2010 - 2015, Met Office
2+
# (C) British Crown Copyright 2010 - 2016, Met Office
33
#
44
# This file is part of Iris.
55
#
@@ -25,17 +25,54 @@
2525
from __future__ import (absolute_import, division, print_function)
2626
from six.moves import (filter, input, map, range, zip) # noqa
2727

28+
import argparse
29+
import codecs
30+
import contextlib
31+
from glob import glob
32+
import hashlib
33+
import json
2834
import os.path
29-
import shutil
3035
import sys
36+
import warnings
3137

38+
from PIL import Image
39+
import filelock
3240
import matplotlib.pyplot as plt
3341
import matplotlib.image as mimg
42+
import matplotlib.testing.compare as mcompare
3443
import matplotlib.widgets as mwidget
35-
36-
37-
def diff_viewer(expected_fname, result_fname, diff_fname):
38-
plt.figure(figsize=(16, 16))
44+
import requests
45+
46+
# Force iris.tests to use the ```tkagg``` backend by using the '-d'
47+
# command-line argument as idiff is an interactive tool that requires a
48+
# gui interface.
49+
sys.argv.append('-d')
50+
import iris.tests
51+
import iris.util as iutil
52+
53+
54+
_POSTFIX_DIFF = '-failed-diff.png'
55+
_POSTFIX_JSON = os.path.join('results', 'imagerepo.json')
56+
_POSTFIX_LOCK = os.path.join('results', 'imagerepo.lock')
57+
_PREFIX_URI = 'https://scitools.github.io/test-images-scitools/image_files'
58+
_TIMEOUT = 30
59+
_TOL = 0
60+
61+
62+
@contextlib.contextmanager
63+
def temp_png(suffix=''):
64+
if suffix:
65+
suffix = '-{}'.format(suffix)
66+
fname = iutil.create_temp_filename(suffix+'.png')
67+
try:
68+
yield fname
69+
finally:
70+
os.remove(fname)
71+
72+
73+
def diff_viewer(repo, key, repo_fname,
74+
expected_fname, result_fname, diff_fname):
75+
plt.figure(figsize=(14, 12))
3976
plt.suptitle(os.path.basename(expected_fname))
4077
ax = plt.subplot(221)
4178
ax.imshow(mimg.imread(expected_fname))
@@ -44,49 +81,126 @@ def diff_viewer(expected_fname, result_fname, diff_fname):
4481
ax = plt.subplot(223, sharex=ax, sharey=ax)
4582
ax.imshow(mimg.imread(diff_fname))
4683

84+
# Determine the new image hash.png name.
85+
with open(result_fname, 'rb') as fi:
86+
sha1 = hashlib.sha1(fi.read())
87+
result_dir = os.path.dirname(result_fname)
88+
fname = sha1.hexdigest() + '.png'
89+
uri = os.path.join(_PREFIX_URI, fname)
90+
hash_fname = os.path.join(result_dir, fname)
91+
4792
def accept(event):
48-
# removes the expected result, and move the most recent result in
49-
print('ACCEPTED NEW FILE: %s' % (os.path.basename(expected_fname), ))
50-
os.remove(expected_fname)
51-
shutil.copy2(result_fname, expected_fname)
93+
if uri not in repo[key]:
94+
# Ensure to maintain strict time order where the first uri
95+
# associated with the repo key is the oldest, and the last
96+
# uri is the youngest
97+
repo[key].append(uri)
98+
# Update the image repo.
99+
with open(repo_fname, 'wb') as fo:
100+
json.dump(repo, fo, indent=4, sort_keys=True)
101+
os.rename(result_fname, hash_fname)
102+
msg = 'ACCEPTED: {} -> {}'
103+
print(msg.format(os.path.basename(result_fname),
104+
os.path.basename(hash_fname)))
105+
else:
106+
msg = 'DUPLICATE: {} -> {} (ignored)'
107+
print(msg.format(os.path.basename(result_fname),
108+
os.path.basename(hash_fname)))
109+
os.remove(result_fname)
52110
os.remove(diff_fname)
53111
plt.close()
54112

55113
def reject(event):
56-
print('REJECTED: %s' % (os.path.basename(expected_fname), ))
114+
if uri not in repo[key]:
115+
print('REJECTED: {}'.format(os.path.basename(result_fname)))
116+
else:
117+
msg = 'DUPLICATE: {} -> {} (ignored)'
118+
print(msg.format(os.path.basename(result_fname),
119+
os.path.basename(hash_fname)))
120+
os.remove(result_fname)
121+
os.remove(diff_fname)
57122
plt.close()
58123

59-
ax_accept = plt.axes([0.7, 0.05, 0.1, 0.075])
60-
ax_reject = plt.axes([0.81, 0.05, 0.1, 0.075])
61-
bnext = mwidget.Button(ax_accept, 'Accept change')
62-
bnext.on_clicked(accept)
63-
bprev = mwidget.Button(ax_reject, 'Reject')
64-
bprev.on_clicked(reject)
124+
def skip(event):
125+
print('SKIPPED: {}'.format(os.path.basename(result_fname)))
126+
os.remove(diff_fname)
127+
plt.close()
65128

129+
ax_accept = plt.axes([0.59, 0.05, 0.1, 0.075])
130+
ax_reject = plt.axes([0.7, 0.05, 0.1, 0.075])
131+
ax_skip = plt.axes([0.81, 0.05, 0.1, 0.075])
132+
baccept = mwidget.Button(ax_accept, 'Accept')
133+
baccept.on_clicked(accept)
134+
breject = mwidget.Button(ax_reject, 'Reject')
135+
breject.on_clicked(reject)
136+
bskip = mwidget.Button(ax_skip, 'Skip')
137+
bskip.on_clicked(skip)
66138
plt.show()
67139

68140

69-
def step_over_diffs():
70-
import iris.tests
71-
image_dir = os.path.join(os.path.dirname(iris.tests.__file__),
72-
'results', 'visual_tests')
73-
diff_dir = os.path.join(os.path.dirname(iris.tests.__file__),
74-
'result_image_comparison')
75-
76-
for expected_fname in sorted(os.listdir(image_dir)):
77-
result_path = os.path.join(diff_dir, 'result-' + expected_fname)
78-
diff_path = result_path[:-4] + '-failed-diff.png'
79-
80-
# if the test failed, there will be a diff file
81-
if os.path.exists(diff_path):
82-
expected_path = os.path.join(image_dir, expected_fname)
83-
diff_viewer(expected_path, result_path, diff_path)
141+
def step_over_diffs(result_dir):
142+
processed = False
143+
dname = os.path.dirname(iris.tests.__file__)
144+
lock = filelock.FileLock(os.path.join(dname, _POSTFIX_LOCK))
145+
146+
# Remove old image diff results.
147+
target = os.path.join(result_dir, '*{}'.format(_POSTFIX_DIFF))
148+
for fname in glob(target):
149+
os.remove(fname)
150+
151+
with lock.acquire(timeout=_TIMEOUT):
152+
# Load the imagerepo.
153+
repo_fname = os.path.join(dname, _POSTFIX_JSON)
154+
with open(repo_fname, 'rb') as fi:
155+
repo = json.load(codecs.getreader('utf-8')(fi))
156+
157+
target = os.path.join(result_dir, 'result-*.png')
158+
for result_fname in sorted(glob(target)):
159+
# We only care about PNG images.
160+
try:
161+
im = Image.open(result_fname)
162+
if im.format != 'PNG':
163+
continue
164+
except IOError:
165+
continue
166+
key = os.path.splitext('-'.join(result_fname.split('-')[1:]))[0]
167+
try:
168+
uri = repo[key][0]
169+
except KeyError:
170+
wmsg = 'Ignoring unregistered test result {!r}.'
171+
warnings.warn(wmsg.format(key))
172+
continue
173+
with temp_png(key) as expected_fname:
174+
processed = True
175+
resource = requests.get(uri)
176+
with open(expected_fname, 'wb') as fo:
177+
fo.write(resource.content)
178+
mcompare.compare_images(expected_fname, result_fname, tol=_TOL)
179+
diff_fname = os.path.splitext(result_fname)[0] + _POSTFIX_DIFF
180+
diff_viewer(repo, key, repo_fname, expected_fname,
181+
result_fname, diff_fname)
182+
if not processed:
183+
msg = '\n{}: There are no iris test result images to process.\n'
184+
print(msg.format(os.path.splitext(sys.argv[0])[0]))
84185

85186

86187
if __name__ == '__main__':
87-
# Force iris.tests to use the ```tkagg``` backend by using the '-d'
88-
# command-line argument as idiff is an interactive tool that requires a
89-
# gui interface.
90-
sys.argv.append('-d')
91-
92-
step_over_diffs()
188+
default = os.path.join(os.path.dirname(iris.tests.__file__),
189+
'result_image_comparison')
190+
description = 'Iris graphic test difference tool.'
191+
parser = argparse.ArgumentParser(description=description)
192+
help = 'Path to iris tests result image directory (default: %(default)s)'
193+
parser.add_argument('--resultdir', '-r',
194+
default=default,
195+
help=help)
196+
help = 'Force "iris.tests" to use the tkagg backend (default: %(default)s)'
197+
parser.add_argument('-d',
198+
action='store_true',
199+
default=True,
200+
help=help)
201+
args = parser.parse_args()
202+
result_dir = args.resultdir
203+
if not os.path.isdir(result_dir):
204+
emsg = 'Invalid results directory: {}'
205+
raise ValueError(emsg.format(result_dir))
206+
step_over_diffs(result_dir)

minimal-conda-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mock
1919
nose
2020
pep8=1.5.7
2121
sphinx
22+
filelock
2223

2324
# Optional iris dependencies
2425
gdal

0 commit comments

Comments
 (0)