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