• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python2.7
2#
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.
6#
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.
12
13"""
14Testing dec2flt
15===============
16These are *really* extensive tests. Expect them to run for hours. Due to the
17nature of the problem (the input is a string of arbitrary length), exhaustive
18testing is not really possible. Instead, there are exhaustive tests for some
19classes of inputs for which that is feasible and a bunch of deterministic and
20random non-exhaustive tests for covering everything else.
21
22The actual tests (generating decimal strings and feeding them to dec2flt) is
23performed by a set of stand-along rust programs. This script compiles, runs,
24and supervises them. The programs report the strings they generate and the
25floating point numbers they converted those strings to, and this script
26checks that the results are correct.
27
28You can run specific tests rather than all of them by giving their names
29(without .rs extension) as command line parameters.
30
31Verification
32------------
33The tricky part is not generating those inputs but verifying the outputs.
34Comparing with the result of Python's float() does not cut it because
35(and this is apparently undocumented) although Python includes a version of
36Martin Gay's code including the decimal-to-float part, it doesn't actually use
37it for float() (only for round()) instead relying on the system scanf() which
38is not necessarily completely accurate.
39
40Instead, we take the input and compute the true value with bignum arithmetic
41(as a fraction, using the ``fractions`` module).
42
43Given an input string and the corresponding float computed via Rust, simply
44decode the float into f * 2^k (for integers f, k) and the ULP.
45We can now easily compute the error and check if it is within 0.5 ULP as it
46should be. Zero and infinites are handled similarly:
47
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.
56
57Implementation details
58----------------------
59This directory contains a set of single-file Rust programs that perform
60tests with a particular class of inputs. Each is compiled and run without
61parameters, outputs (f64, f32, decimal) pairs to verify externally, and
62in any case either exits gracefully or with a panic.
63
64If a test binary writes *anything at all* to stderr or exits with an
65exit code that's not 0, the test fails.
66The output on stdout is treated as (f64, f32, decimal) record, encoded thusly:
67
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.
72
73Incomplete records are an error. Not-a-Number bit patterns are invalid too.
74
75The tests run serially but the validation for a single test is parallelized
76with ``multiprocessing``. Each test is launched as a subprocess.
77One thread supervises it: Accepts and enqueues records to validate, observe
78stderr, and waits for the process to exit. A set of worker processes perform
79the validation work for the outputs enqueued there. Another thread listens
80for progress updates from the workers.
81
82Known issues
83------------
84Some errors (e.g., NaN outputs) aren't handled very gracefully.
85Also, if there is an exception or the process is interrupted (at least on
86Windows) the worker processes are leaked and stick around forever.
87They're only a few megabytes each, but still, this script should not be run
88if you aren't prepared to manually kill a lot of orphaned processes.
89"""
90from __future__ import print_function
91import sys
92import os
93import time
94import struct
95from fractions import Fraction
96from collections import namedtuple
97from subprocess import Popen, check_call, PIPE
98from glob import glob
99import multiprocessing
100import threading
101import ctypes
102import binascii
103
104try:  # Python 3
105    import queue as Queue
106except ImportError:  # Python 2
107    import Queue
108
109NUM_WORKERS = 2
110UPDATE_EVERY_N = 50000
111INF = namedtuple('INF', '')()
112NEG_INF = namedtuple('NEG_INF', '')()
113ZERO = namedtuple('ZERO', '')()
114MAILBOX = None  # The queue for reporting errors to the main process.
115STDOUT_LOCK = threading.Lock()
116test_name = None
117child_processes = []
118exit_status = 0
119
120def msg(*args):
121    with STDOUT_LOCK:
122        print("[" + test_name + "]", *args)
123        sys.stdout.flush()
124
125
126def write_errors():
127    global exit_status
128    f = open("errors.txt", 'w')
129    have_seen_error = False
130    while True:
131        args = MAILBOX.get()
132        if args is None:
133            f.close()
134            break
135        print(*args, file=f)
136        f.flush()
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")
141            exit_status = 101
142
143def projectdir():
144    file = os.path.realpath(__file__)
145    return os.path.dirname(os.path.dirname(file))
146
147def targetdir():
148    return os.path.join(projectdir(), 'target')
149
150def releasedir():
151    return os.path.join(targetdir(), 'release')
152
153def cargo():
154    path = os.getcwd()
155    os.chdir(projectdir())
156    check_call(['cargo', 'build', '--release'])
157    os.chdir(path)
158
159
160def run(test):
161    global test_name
162    test_name = test
163
164    t0 = time.perf_counter()
165    msg("setting up supervisor")
166    command = ['cargo', 'run', '--bin', test, '--release']
167    proc = Popen(command, bufsize=1<<20 , stdin=PIPE, stdout=PIPE, stderr=PIPE)
168    done = multiprocessing.Value(ctypes.c_bool)
169    queue = multiprocessing.Queue(maxsize=5)#(maxsize=1024)
170    workers = []
171    for n in range(NUM_WORKERS):
172        worker = multiprocessing.Process(name='Worker-' + str(n + 1),
173                                         target=init_worker,
174                                         args=[test, MAILBOX, queue, done])
175        workers.append(worker)
176        child_processes.append(worker)
177    for worker in workers:
178        worker.start()
179    msg("running test")
180    interact(proc, queue)
181    with done.get_lock():
182        done.value = True
183    for worker in workers:
184        worker.join()
185    msg("python is done")
186    assert queue.empty(), "did not validate everything"
187    dt = time.perf_counter() - t0
188    msg("took", round(dt, 3), "seconds")
189
190
191def interact(proc, queue):
192    n = 0
193    while proc.poll() is None:
194        line = proc.stdout.readline()
195        if not line:
196            continue
197        assert line.endswith(b'\n'), "incomplete line: " + repr(line)
198        queue.put(line)
199        n += 1
200        if n % UPDATE_EVERY_N == 0:
201            msg("got", str(n // 1000) + "k", "records")
202    msg("rust is done. exit code:", proc.returncode)
203    rest, stderr = proc.communicate()
204    if stderr:
205        msg("rust stderr output:", stderr)
206    for line in rest.split(b'\n'):
207        if not line:
208            continue
209        queue.put(line)
210
211
212def main():
213    global MAILBOX
214    files = glob(f'{projectdir()}/test-parse-random/*.rs')
215    basenames = [os.path.basename(i) for i in files]
216    all_tests = [os.path.splitext(f)[0] for f in basenames if not f.startswith('_')]
217    args = sys.argv[1:]
218    if args:
219        tests = [test for test in all_tests if test in args]
220    else:
221        tests = all_tests
222    if not tests:
223        print("Error: No tests to run")
224        sys.exit(1)
225    # Compile first for quicker feedback
226    cargo()
227    # Set up mailbox once for all tests
228    MAILBOX = multiprocessing.Queue()
229    mailman = threading.Thread(target=write_errors)
230    mailman.daemon = True
231    mailman.start()
232    for test in tests:
233        run(test)
234    MAILBOX.put(None)
235    mailman.join()
236
237
238# ---- Worker thread code ----
239
240
241POW2 = { e: Fraction(2) ** e for e in range(-1100, 1100) }
242HALF_ULP = { e: (Fraction(2) ** e)/2 for e in range(-1100, 1100) }
243DONE_FLAG = None
244
245
246def send_error_to_supervisor(*args):
247    MAILBOX.put(args)
248
249
250def init_worker(test, mailbox, queue, done):
251    global test_name, MAILBOX, DONE_FLAG
252    test_name = test
253    MAILBOX = mailbox
254    DONE_FLAG = done
255    do_work(queue)
256
257
258def is_done():
259    with DONE_FLAG.get_lock():
260        return DONE_FLAG.value
261
262
263def do_work(queue):
264    while True:
265        try:
266            line = queue.get(timeout=0.01)
267        except Queue.Empty:
268            if queue.empty() and is_done():
269                return
270            else:
271                continue
272        bin64, bin32, text = line.rstrip().split()
273        validate(bin64, bin32, text.decode('utf-8'))
274
275
276def decode_binary64(x):
277    """
278    Turn a IEEE 754 binary64 into (mantissa, exponent), except 0.0 and
279    infinity (positive and negative), which return ZERO, INF, and NEG_INF
280    respectively.
281    """
282    x = binascii.unhexlify(x)
283    assert len(x) == 8, repr(x)
284    [bits] = struct.unpack(b'>Q', x)
285    if bits == 0:
286        return ZERO
287    exponent = (bits >> 52) & 0x7FF
288    negative = bits >> 63
289    low_bits = bits & 0xFFFFFFFFFFFFF
290    if exponent == 0:
291        mantissa = low_bits
292        exponent += 1
293        if mantissa == 0:
294            return ZERO
295    elif exponent == 0x7FF:
296        assert low_bits == 0, "NaN"
297        if negative:
298            return NEG_INF
299        else:
300            return INF
301    else:
302        mantissa = low_bits | (1 << 52)
303    exponent -= 1023 + 52
304    if negative:
305        mantissa = -mantissa
306    return (mantissa, exponent)
307
308
309def decode_binary32(x):
310    """
311    Turn a IEEE 754 binary32 into (mantissa, exponent), except 0.0 and
312    infinity (positive and negative), which return ZERO, INF, and NEG_INF
313    respectively.
314    """
315    x = binascii.unhexlify(x)
316    assert len(x) == 4, repr(x)
317    [bits] = struct.unpack(b'>I', x)
318    if bits == 0:
319        return ZERO
320    exponent = (bits >> 23) & 0xFF
321    negative = bits >> 31
322    low_bits = bits & 0x7FFFFF
323    if exponent == 0:
324        mantissa = low_bits
325        exponent += 1
326        if mantissa == 0:
327            return ZERO
328    elif exponent == 0xFF:
329        if negative:
330            return NEG_INF
331        else:
332            return INF
333    else:
334        mantissa = low_bits | (1 << 23)
335    exponent -= 127 + 23
336    if negative:
337        mantissa = -mantissa
338    return (mantissa, exponent)
339
340
341MIN_SUBNORMAL_DOUBLE = Fraction(2) ** -1074
342MIN_SUBNORMAL_SINGLE = Fraction(2) ** -149  # XXX unsure
343MAX_DOUBLE = (2 - Fraction(2) ** -52) * (2 ** 1023)
344MAX_SINGLE = (2 - Fraction(2) ** -23) * (2 ** 127)
345MAX_ULP_DOUBLE = 1023 - 52
346MAX_ULP_SINGLE = 127 - 23
347DOUBLE_ZERO_CUTOFF = MIN_SUBNORMAL_DOUBLE / 2
348DOUBLE_INF_CUTOFF = MAX_DOUBLE + 2 ** (MAX_ULP_DOUBLE - 1)
349SINGLE_ZERO_CUTOFF = MIN_SUBNORMAL_SINGLE / 2
350SINGLE_INF_CUTOFF = MAX_SINGLE + 2 ** (MAX_ULP_SINGLE - 1)
351
352def validate(bin64, bin32, text):
353    try:
354        double = decode_binary64(bin64)
355    except AssertionError:
356        print(bin64, bin32, text)
357        raise
358    single = decode_binary32(bin32)
359    real = Fraction(text)
360
361    if double is ZERO:
362        if real > DOUBLE_ZERO_CUTOFF:
363            record_special_error(text, "f64 zero")
364    elif double is INF:
365        if real < DOUBLE_INF_CUTOFF:
366            record_special_error(text, "f64 inf")
367    elif double is NEG_INF:
368        if -real < DOUBLE_INF_CUTOFF:
369            record_special_error(text, "f64 -inf")
370    elif len(double) == 2:
371        sig, k = double
372        validate_normal(text, real, sig, k, "f64")
373    else:
374        assert 0, "didn't handle binary64"
375    if single is ZERO:
376        if real > SINGLE_ZERO_CUTOFF:
377            record_special_error(text, "f32 zero")
378    elif single is INF:
379        if real < SINGLE_INF_CUTOFF:
380            record_special_error(text, "f32 inf")
381    elif single is NEG_INF:
382        if -real < SINGLE_INF_CUTOFF:
383            record_special_error(text, "f32 -inf")
384    elif len(single) == 2:
385        sig, k = single
386        validate_normal(text, real, sig, k, "f32")
387    else:
388        assert 0, "didn't handle binary32"
389
390def record_special_error(text, descr):
391    send_error_to_supervisor(text.strip(), "wrongly rounded to", descr)
392
393
394def validate_normal(text, real, sig, k, kind):
395    approx = sig * POW2[k]
396    error = abs(approx - real)
397    if error > HALF_ULP[k]:
398        record_normal_error(text, error, k, kind)
399
400
401def record_normal_error(text, error, k, kind):
402    one_ulp = HALF_ULP[k + 1]
403    assert one_ulp == 2 * HALF_ULP[k]
404    relative_error = error / one_ulp
405    text = text.strip()
406    try:
407        err_repr = float(relative_error)
408    except ValueError:
409        err_repr = str(err_repr).replace('/', ' / ')
410    send_error_to_supervisor(err_repr, "ULP error on", text, "(" + kind + ")")
411
412
413if __name__ == '__main__':
414    main()
415