1 #!/usr/bin/env python2.7
3 # Copyright 2015 The Rust Project Developers. See the COPYRIGHT
4 # file at the top-level directory of this distribution and at
5 # http://rust-lang.org/COPYRIGHT.
7 # Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
8 # http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
9 # <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
10 # option. This file may not be copied, modified, or distributed
11 # except according to those terms.
16 These are *really* extensive tests. Expect them to run for hours. Due to the
17 nature of the problem (the input is a string of arbitrary length), exhaustive
18 testing is not really possible. Instead, there are exhaustive tests for some
19 classes of inputs for which that is feasible and a bunch of deterministic and
20 random non-exhaustive tests for covering everything else.
22 The actual tests (generating decimal strings and feeding them to dec2flt) is
23 performed by a set of stand-along rust programs. This script compiles, runs,
24 and supervises them. The programs report the strings they generate and the
25 floating point numbers they converted those strings to, and this script
26 checks that the results are correct.
28 You can run specific tests rather than all of them by giving their names
29 (without .rs extension) as command line parameters.
33 The tricky part is not generating those inputs but verifying the outputs.
34 Comparing with the result of Python's float() does not cut it because
35 (and this is apparently undocumented) although Python includes a version of
36 Martin Gay's code including the decimal-to-float part, it doesn't actually use
37 it for float() (only for round()) instead relying on the system scanf() which
38 is not necessarily completely accurate.
40 Instead, we take the input and compute the true value with bignum arithmetic
41 (as a fraction, using the ``fractions`` module).
43 Given an input string and the corresponding float computed via Rust, simply
44 decode the float into f * 2^k (for integers f, k) and the ULP.
45 We can now easily compute the error and check if it is within 0.5 ULP as it
46 should be. Zero and infinites are handled similarly:
48 - If the approximation is 0.0, the exact value should be *less or equal*
49 half the smallest denormal float: the smallest denormal floating point
50 number has an odd mantissa (00...001) and thus half of that is rounded
51 to 00...00, i.e., zero.
52 - If the approximation is Inf, the exact value should be *greater or equal*
53 to the largest finite float + 0.5 ULP: the largest finite float has an odd
54 mantissa (11...11), so that plus half an ULP is rounded up to the nearest
55 even number, which overflows.
57 Implementation details
58 ----------------------
59 This directory contains a set of single-file Rust programs that perform
60 tests with a particular class of inputs. Each is compiled and run without
61 parameters, outputs (f64, f32, decimal) pairs to verify externally, and
62 in any case either exits gracefully or with a panic.
64 If a test binary writes *anything at all* to stderr or exits with an
65 exit code that's not 0, the test fails.
66 The output on stdout is treated as (f64, f32, decimal) record, encoded thusly:
68 - First, the bits of the f64 encoded as an ASCII hex string.
69 - Second, the bits of the f32 encoded as an ASCII hex string.
70 - Then the corresponding string input, in ASCII
71 - The record is terminated with a newline.
73 Incomplete records are an error. Not-a-Number bit patterns are invalid too.
75 The tests run serially but the validation for a single test is parallelized
76 with ``multiprocessing``. Each test is launched as a subprocess.
77 One thread supervises it: Accepts and enqueues records to validate, observe
78 stderr, and waits for the process to exit. A set of worker processes perform
79 the validation work for the outputs enqueued there. Another thread listens
80 for progress updates from the workers.
84 Some errors (e.g., NaN outputs) aren't handled very gracefully.
85 Also, if there is an exception or the process is interrupted (at least on
86 Windows) the worker processes are leaked and stick around forever.
87 They're only a few megabytes each, but still, this script should not be run
88 if you aren't prepared to manually kill a lot of orphaned processes.
90 from __future__ import print_function
95 from fractions import Fraction
96 from collections import namedtuple
97 from subprocess import Popen, check_call, PIPE
99 import multiprocessing
105 import queue as Queue
106 except ImportError: # Python 2
110 UPDATE_EVERY_N = 50000
111 INF = namedtuple('INF', '')()
112 NEG_INF = namedtuple('NEG_INF', '')()
113 ZERO = namedtuple('ZERO', '')()
114 MAILBOX = None # The queue for reporting errors to the main process.
115 STDOUT_LOCK = threading.Lock()
122 print("[" + test_name + "]", *args)
128 f = open("errors.txt", 'w')
129 have_seen_error = False
137 if not have_seen_error:
138 have_seen_error = True
139 msg("Something is broken:", *args)
140 msg("Future errors logged to errors.txt")
146 exe = test + '.exe' # hopefully this makes it work on *nix
147 print("compiling", test)
149 check_call(['rustc', rs, '-o', exe])
157 msg("setting up supervisor")
159 proc = Popen(exe, bufsize=1<<20 , stdin=PIPE, stdout=PIPE, stderr=PIPE)
160 done = multiprocessing.Value(ctypes.c_bool)
161 queue = multiprocessing.Queue(maxsize=5)#(maxsize=1024)
163 for n in range(NUM_WORKERS):
164 worker = multiprocessing.Process(name='Worker-' + str(n + 1),
166 args=[test, MAILBOX, queue, done])
167 workers.append(worker)
168 child_processes.append(worker)
169 for worker in workers:
172 interact(proc, queue)
173 with done.get_lock():
175 for worker in workers:
177 msg("python is done")
178 assert queue.empty(), "did not validate everything"
179 dt = time.clock() - t0
180 msg("took", round(dt, 3), "seconds")
183 def interact(proc, queue):
185 while proc.poll() is None:
186 line = proc.stdout.readline()
189 assert line.endswith('\n'), "incomplete line: " + repr(line)
192 if n % UPDATE_EVERY_N == 0:
193 msg("got", str(n // 1000) + "k", "records")
194 msg("rust is done. exit code:", proc.returncode)
195 rest, stderr = proc.communicate()
197 msg("rust stderr output:", stderr)
198 for line in rest.split('\n'):
206 tests = [os.path.splitext(f)[0] for f in glob('*.rs')
207 if not f.startswith('_')]
208 whitelist = sys.argv[1:]
210 tests = [test for test in tests if test in whitelist]
212 print("Error: No tests to run")
214 # Compile first for quicker feedback
217 # Set up mailbox once for all tests
218 MAILBOX = multiprocessing.Queue()
219 mailman = threading.Thread(target=write_errors)
220 mailman.daemon = True
223 if whitelist and test not in whitelist:
230 # ---- Worker thread code ----
233 POW2 = { e: Fraction(2) ** e for e in range(-1100, 1100) }
234 HALF_ULP = { e: (Fraction(2) ** e)/2 for e in range(-1100, 1100) }
238 def send_error_to_supervisor(*args):
242 def init_worker(test, mailbox, queue, done):
243 global test_name, MAILBOX, DONE_FLAG
251 with DONE_FLAG.get_lock():
252 return DONE_FLAG.value
258 line = queue.get(timeout=0.01)
260 if queue.empty() and is_done():
264 bin64, bin32, text = line.rstrip().split()
265 validate(bin64, bin32, text)
268 def decode_binary64(x):
270 Turn a IEEE 754 binary64 into (mantissa, exponent), except 0.0 and
271 infinity (positive and negative), which return ZERO, INF, and NEG_INF
274 x = binascii.unhexlify(x)
275 assert len(x) == 8, repr(x)
276 [bits] = struct.unpack(b'>Q', x)
279 exponent = (bits >> 52) & 0x7FF
280 negative = bits >> 63
281 low_bits = bits & 0xFFFFFFFFFFFFF
287 elif exponent == 0x7FF:
288 assert low_bits == 0, "NaN"
294 mantissa = low_bits | (1 << 52)
295 exponent -= 1023 + 52
298 return (mantissa, exponent)
301 def decode_binary32(x):
303 Turn a IEEE 754 binary32 into (mantissa, exponent), except 0.0 and
304 infinity (positive and negative), which return ZERO, INF, and NEG_INF
307 x = binascii.unhexlify(x)
308 assert len(x) == 4, repr(x)
309 [bits] = struct.unpack(b'>I', x)
312 exponent = (bits >> 23) & 0xFF
313 negative = bits >> 31
314 low_bits = bits & 0x7FFFFF
320 elif exponent == 0xFF:
326 mantissa = low_bits | (1 << 23)
330 return (mantissa, exponent)
333 MIN_SUBNORMAL_DOUBLE = Fraction(2) ** -1074
334 MIN_SUBNORMAL_SINGLE = Fraction(2) ** -149 # XXX unsure
335 MAX_DOUBLE = (2 - Fraction(2) ** -52) * (2 ** 1023)
336 MAX_SINGLE = (2 - Fraction(2) ** -23) * (2 ** 127)
337 MAX_ULP_DOUBLE = 1023 - 52
338 MAX_ULP_SINGLE = 127 - 23
339 DOUBLE_ZERO_CUTOFF = MIN_SUBNORMAL_DOUBLE / 2
340 DOUBLE_INF_CUTOFF = MAX_DOUBLE + 2 ** (MAX_ULP_DOUBLE - 1)
341 SINGLE_ZERO_CUTOFF = MIN_SUBNORMAL_SINGLE / 2
342 SINGLE_INF_CUTOFF = MAX_SINGLE + 2 ** (MAX_ULP_SINGLE - 1)
344 def validate(bin64, bin32, text):
345 double = decode_binary64(bin64)
346 single = decode_binary32(bin32)
347 real = Fraction(text)
350 if real > DOUBLE_ZERO_CUTOFF:
351 record_special_error(text, "f64 zero")
353 if real < DOUBLE_INF_CUTOFF:
354 record_special_error(text, "f64 inf")
355 elif double is NEG_INF:
356 if -real < DOUBLE_INF_CUTOFF:
357 record_special_error(text, "f64 -inf")
358 elif len(double) == 2:
360 validate_normal(text, real, sig, k, "f64")
362 assert 0, "didn't handle binary64"
364 if real > SINGLE_ZERO_CUTOFF:
365 record_special_error(text, "f32 zero")
367 if real < SINGLE_INF_CUTOFF:
368 record_special_error(text, "f32 inf")
369 elif single is NEG_INF:
370 if -real < SINGLE_INF_CUTOFF:
371 record_special_error(text, "f32 -inf")
372 elif len(single) == 2:
374 validate_normal(text, real, sig, k, "f32")
376 assert 0, "didn't handle binary32"
378 def record_special_error(text, descr):
379 send_error_to_supervisor(text.strip(), "wrongly rounded to", descr)
382 def validate_normal(text, real, sig, k, kind):
383 approx = sig * POW2[k]
384 error = abs(approx - real)
385 if error > HALF_ULP[k]:
386 record_normal_error(text, error, k, kind)
389 def record_normal_error(text, error, k, kind):
390 one_ulp = HALF_ULP[k + 1]
391 assert one_ulp == 2 * HALF_ULP[k]
392 relative_error = error / one_ulp
395 err_repr = float(relative_error)
397 err_repr = str(err_repr).replace('/', ' / ')
398 send_error_to_supervisor(err_repr, "ULP error on", text, "(" + kind + ")")
401 if __name__ == '__main__':