diff options
-rw-r--r-- | src/binary-writer-spec.c | 41 | ||||
-rw-r--r-- | src/tools/wasm-interp.c | 46 | ||||
-rw-r--r-- | test/gen-spec-empty-prefix.js | 1 | ||||
-rwxr-xr-x | test/gen-spec-js.py | 606 | ||||
-rw-r--r-- | test/gen-spec-js/action.txt | 17 | ||||
-rw-r--r-- | test/gen-spec-js/assert_malformed.txt | 10 | ||||
-rw-r--r-- | test/gen-spec-js/assert_return.txt | 48 | ||||
-rw-r--r-- | test/gen-spec-js/assert_return_nan.txt | 16 | ||||
-rw-r--r-- | test/gen-spec-js/assert_trap.txt | 18 | ||||
-rw-r--r-- | test/gen-spec-js/assert_uninstantiable.txt | 12 | ||||
-rw-r--r-- | test/gen-spec-js/assert_unlinkable.txt | 10 | ||||
-rw-r--r-- | test/gen-spec-js/basic.txt | 12 | ||||
-rw-r--r-- | test/gen-spec-js/many-modules.txt | 21 | ||||
-rw-r--r-- | test/gen-spec-js/register.txt | 20 | ||||
-rw-r--r-- | test/gen-spec-prefix.js | 93 | ||||
-rwxr-xr-x | test/run-gen-spec-js.py | 99 | ||||
-rwxr-xr-x | test/run-tests.py | 18 | ||||
-rw-r--r-- | test/utils.py | 5 |
18 files changed, 934 insertions, 159 deletions
diff --git a/src/binary-writer-spec.c b/src/binary-writer-spec.c index 35d52ab1..3a48ccf7 100644 --- a/src/binary-writer-spec.c +++ b/src/binary-writer-spec.c @@ -162,6 +162,13 @@ static void write_var(Context* ctx, const WasmVar* var) { write_escaped_string_slice(ctx, var->name); } +static void write_type_object(Context* ctx, WasmType type) { + wasm_writef(&ctx->json_stream, "{"); + write_key(ctx, "type"); + write_string(ctx, wasm_get_type_name(type)); + wasm_writef(&ctx->json_stream, "}"); +} + static void write_const(Context* ctx, const WasmConst* const_) { wasm_writef(&ctx->json_stream, "{"); write_key(ctx, "type"); @@ -249,6 +256,36 @@ static void write_action(Context* ctx, const WasmAction* action) { wasm_writef(&ctx->json_stream, "}"); } +static void write_action_result_type(Context* ctx, + WasmScript* script, + const WasmAction* action) { + const WasmModule* module = + wasm_get_module_by_var(script, &action->module_var); + const WasmExport* export; + wasm_writef(&ctx->json_stream, "["); + switch (action->type) { + case WASM_ACTION_TYPE_INVOKE: { + export = wasm_get_export_by_name(module, &action->invoke.name); + assert(export->kind == WASM_EXTERNAL_KIND_FUNC); + WasmFunc* func = wasm_get_func_by_var(module, &export->var); + size_t num_results = wasm_get_num_results(func); + size_t i; + for (i = 0; i < num_results; ++i) + write_type_object(ctx, wasm_get_result_type(func, i)); + break; + } + + case WASM_ACTION_TYPE_GET: { + export = wasm_get_export_by_name(module, &action->get.name); + assert(export->kind == WASM_EXTERNAL_KIND_GLOBAL); + WasmGlobal* global = wasm_get_global_by_var(module, &export->var); + write_type_object(ctx, global->type); + break; + } + } + wasm_writef(&ctx->json_stream, "]"); +} + static void write_module(Context* ctx, char* filename, const WasmModule* module) { @@ -398,6 +435,10 @@ static void write_commands(Context* ctx, WasmScript* script) { write_location(ctx, &command->assert_return_nan.action.loc); write_separator(ctx); write_action(ctx, &command->assert_return_nan.action); + write_separator(ctx); + write_key(ctx, "expected"); + write_action_result_type(ctx, script, + &command->assert_return_nan.action); break; case WASM_COMMAND_TYPE_ASSERT_TRAP: diff --git a/src/tools/wasm-interp.c b/src/tools/wasm-interp.c index 9956c614..2180762e 100644 --- a/src/tools/wasm-interp.c +++ b/src/tools/wasm-interp.c @@ -909,6 +909,46 @@ static WasmResult parse_line(Context* ctx) { return WASM_OK; } +static WasmResult parse_type_object(Context* ctx, WasmType* out_type) { + WasmStringSlice type_str; + EXPECT("{"); + PARSE_KEY_STRING_VALUE("type", &type_str); + EXPECT("}"); + + if (string_slice_equals_str(&type_str, "i32")) { + *out_type = WASM_TYPE_I32; + return WASM_OK; + } else if (string_slice_equals_str(&type_str, "f32")) { + *out_type = WASM_TYPE_F32; + return WASM_OK; + } else if (string_slice_equals_str(&type_str, "i64")) { + *out_type = WASM_TYPE_I64; + return WASM_OK; + } else if (string_slice_equals_str(&type_str, "f64")) { + *out_type = WASM_TYPE_F64; + return WASM_OK; + } else { + print_parse_error(ctx, "unknown type: \"" PRIstringslice "\"", + WASM_PRINTF_STRING_SLICE_ARG(type_str)); + return WASM_ERROR; + } +} + +static WasmResult parse_type_vector(Context* ctx, WasmTypeVector* out_types) { + WASM_ZERO_MEMORY(*out_types); + EXPECT("["); + WasmBool first = WASM_TRUE; + while (!match(ctx, "]")) { + if (!first) + EXPECT(","); + WasmType type; + CHECK_RESULT(parse_type_object(ctx, &type)); + first = WASM_FALSE; + wasm_append_type_value(ctx->allocator, out_types, &type); + } + return WASM_OK; +} + static WasmResult parse_const(Context* ctx, WasmInterpreterTypedValue* out_value) { WasmStringSlice type_str; @@ -1457,13 +1497,19 @@ static WasmResult parse_command(Context* ctx) { destroy_action(ctx->allocator, &action); } else if (match(ctx, "\"assert_return_nan\"")) { Action action; + WasmTypeVector expected; WASM_ZERO_MEMORY(action); EXPECT(","); CHECK_RESULT(parse_line(ctx)); EXPECT(","); CHECK_RESULT(parse_action(ctx, &action)); + EXPECT(","); + /* Not needed for wasm-interp, but useful for other parsers. */ + EXPECT_KEY("expected"); + CHECK_RESULT(parse_type_vector(ctx, &expected)); on_assert_return_nan_command(ctx, &action); + wasm_destroy_type_vector(ctx->allocator, &expected); destroy_action(ctx->allocator, &action); } else if (match(ctx, "\"assert_trap\"")) { Action action; diff --git a/test/gen-spec-empty-prefix.js b/test/gen-spec-empty-prefix.js new file mode 100644 index 00000000..6ed02d3a --- /dev/null +++ b/test/gen-spec-empty-prefix.js @@ -0,0 +1 @@ +// A deliberately empty file for testing. diff --git a/test/gen-spec-js.py b/test/gen-spec-js.py index 45e72c3d..cd3f3d75 100755 --- a/test/gen-spec-js.py +++ b/test/gen-spec-js.py @@ -16,181 +16,473 @@ # import argparse +from collections import namedtuple import cStringIO import json import os +import re +import struct import sys -from utils import Error - -JS_HEADER = """\ -var passed = 0; -var failed = 0; -var quiet = false; - -""" - -MODULE_RUN = """\ -testModule%(module_index)s(); -""" - -MODULE_RUN_FOOTER = """\ -end(); - -""" - -MODULE_TEST_HEADER = """\ -function testModule%(module_index)s() { -""" - -MODULE_DATA_HEADER = """\ - var module = createModule([ -""" - -MODULE_DATA_FOOTER = """\ - ]); - -""" - -INVOKE_COMMAND = """\ - invoke(module, '%(name)s'); -""" - -ASSERT_RETURN_COMMAND = """\ - assertReturn(module, '%(name)s', '%(file)s', %(line)s); -""" - -ASSERT_TRAP_COMMAND = """\ - assertTrap(module, '%(name)s', '%(file)s', %(line)s); -""" - -COMMANDS = { - 'invoke': INVOKE_COMMAND, - 'assert_return': ASSERT_RETURN_COMMAND, - 'assert_return_nan': ASSERT_RETURN_COMMAND, - 'assert_trap': ASSERT_TRAP_COMMAND -} - -MODULE_TEST_FOOTER = """\ -} - -""" - -JS_FOOTER = """\ -function createModule(data) { - var u8a = new Uint8Array(data); - var ffi = {spectest: {print: print}}; - return Wasm.instantiateModule(u8a, ffi); -} - -function assertReturn(module, name, file, line) { - try { - var result = module.exports[name](); - } catch(e) { - print(file + ":" + line + ": " + name + " unexpectedly threw: " + e); - } - - if (result == 1) { - passed++; - } else { - print(file + ":" + line + ": " + name + " failed."); - failed++; - } -} - -function assertTrap(module, name, file, line) { - var threw = false; - try { - module.exports[name](); - } catch (e) { - threw = true; - } - - if (threw) { - passed++; - } else { - print(file + ":" + line + ": " + name + " failed, didn't throw"); - failed++; - } -} - -function invoke(module, name) { - try { - var invokeResult = module.exports[name](); - passed++; - } catch(e) { - print(name + " unexpectedly threw: " + e); - failed++; - } - - if (!quiet) - print(name + " = " + invokeResult); -} - -function end() { - if ((failed > 0) || !quiet) - print(passed + "/" + (passed + failed) + " tests passed."); -} -""" - -def ProcessJsonFile(json_file_path): - json_file_dir = os.path.dirname(json_file_path) - with open(json_file_path) as json_file: - json_data = json.load(json_file) - - output = cStringIO.StringIO() - output.write(JS_HEADER) - for module_index in range(len(json_data['modules'])): - output.write(MODULE_RUN % {'module_index': module_index}) - output.write(MODULE_RUN_FOOTER) - - for module_index, module in enumerate(json_data['modules']): - module_filepath = os.path.join(json_file_dir, module['filename']) - with open(module_filepath, 'rb') as wasm_file: - wasm_data = wasm_file.read() - - output.write(MODULE_TEST_HEADER % {'module_index': module_index}) - output.write(MODULE_DATA_HEADER) - WriteModuleBytes(output, wasm_data) - output.write(MODULE_DATA_FOOTER) - - for command_index, command in enumerate(module['commands']): - output.write(COMMANDS[command['type']] % command) - output.write(MODULE_TEST_FOOTER) - output.write(JS_FOOTER) - - return output.getvalue() - - -def WriteModuleBytes(output, module_data): - BYTES_PER_LINE = 16 - offset = 0 - while True: - line_data = module_data[offset:offset+BYTES_PER_LINE] - if not line_data: - break - output.write(' ') - output.write(', '.join('%3d' % ord(byte) for byte in line_data)) - output.write(',\n') - offset += BYTES_PER_LINE +from find_exe import GetWast2WasmExecutable, GetWasm2WastExecutable +from utils import ChangeDir, ChangeExt, Error, Executable +import utils + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + +F32_INF = 0x7f800000 +F32_NEG_INF = 0xff800000 +F32_NEG_ZERO = 0x80000000 +F32_SIGN_BIT = F32_NEG_ZERO +F32_SIG_MASK = 0x7fffff +F32_QUIET_NAN_TAG = 0x400000 +F64_INF = 0x7ff0000000000000L +F64_NEG_INF = 0xfff0000000000000L +F64_NEG_ZERO = 0x8000000000000000L +F64_SIGN_BIT = F64_NEG_ZERO +F64_SIG_MASK = 0xfffffffffffffL +F64_QUIET_NAN_TAG = 0x8000000000000L + +def I32ToJS(value): + # JavaScript will return all i32 values as signed. + if value >= 2**31: + value -= 2**32 + return str(value) + +def IsNaNF32(f32_bits): + return (F32_INF < f32_bits < F32_NEG_ZERO) or (f32_bits > F32_NEG_INF) + +def ReinterpretF32(f32_bits): + return struct.unpack('<f', struct.pack('<I', f32_bits))[0] + +def NaNF32ToString(f32_bits): + result = '-' if f32_bits & F32_SIGN_BIT else '' + result += 'nan' + sig = f32_bits & F32_SIG_MASK + if sig != F32_QUIET_NAN_TAG: + result += ':0x%x' % sig + return result + +def F32ToWasm(f32_bits): + if IsNaNF32(f32_bits): + return 'f32.const %s' % NaNF32ToString(f32_bits) + elif f32_bits == F32_INF: + return 'f32.const infinity' + elif f32_bits == F32_NEG_INF: + return 'f32.const -infinity' + else: + return 'f32.const %s' % float.hex(ReinterpretF32(f32_bits)) + +def F32ToJS(f32_bits): + assert not IsNaNF32(f32_bits) + if f32_bits == F32_INF: + return 'Infinity' + elif f32_bits == F32_NEG_INF: + return '-Infinity' + else: + return 'f32(%s)' % ReinterpretF32(f32_bits) + +def IsNaNF64(f64_bits): + return (F64_INF < f64_bits < F64_NEG_ZERO) or (f64_bits > F64_NEG_INF) + +def ReinterpretF64(f64_bits): + return struct.unpack('<d', struct.pack('<Q', f64_bits))[0] + +def NaNF64ToString(f64_bits): + result = '-' if f64_bits & F64_SIGN_BIT else '' + result += 'nan' + sig = f64_bits & F64_SIG_MASK + if sig != F64_QUIET_NAN_TAG: + result += ':0x%x' % sig + return result + +def F64ToWasm(f64_bits): + if IsNaNF64(f64_bits): + return 'f64.const %s' % NaNF64ToString(f64_bits) + elif f64_bits == F64_INF: + return 'f64.const infinity' + elif f64_bits == F64_NEG_INF: + return 'f64.const -infinity' + else: + return 'f64.const %s' % float.hex(ReinterpretF64(f64_bits)) + +def F64ToJS(f64_bits): + assert not IsNaNF64(f64_bits) + if f64_bits == F64_INF: + return 'Infinity' + elif f64_bits == F64_NEG_INF: + return '-Infinity' + else: + # Use repr to get full precision + return repr(ReinterpretF64(f64_bits)) + + +def UnescapeWasmString(s): + # Wast allows for more escape characters than this, but we assume that + # wasm2wast will only use the \xx escapes. + result = '' + i = 0 + while i < len(s): + c = s[i] + if c == '\\': + x = s[i+1:i+3] + if len(x) != 2: + raise Error('String with invalid escape: \"%s\"' % s) + result += chr(int(x, 16)) + i += 3 + else: + result += c + i += 1 + return result + +def EscapeJSString(s): + result = '' + for c in s: + if 32 <= ord(c) < 127 and c not in '"\\': + result += c + else: + result += '\\x%02x' % ord(c) + return result + + +def IsValidJSConstant(const): + type_ = const['type'] + if type_ == 'i32': + return True + elif type_ == 'i64': + return False + elif type_ == 'f32': + return not IsNaNF32(int(const['value'])) + elif type_ == 'f64': + return not IsNaNF64(long(const['value'])) + +def IsValidJSAction(action): + return all(IsValidJSConstant(x) for x in action.get('args', [])) + +def IsValidJSCommand(command): + type_ = command['type'] + action = command['action'] + if type_ == 'assert_return': + expected = command['expected'] + return (IsValidJSAction(action) and + all(IsValidJSConstant(x) for x in expected)) + elif type_ == 'assert_return_nan': + return IsValidJSAction(action) + elif type_ == 'assert_trap': + return IsValidJSAction(action) + + +def CollectInvalidModuleCommands(commands): + modules = [] + module_map = {} + for command in commands: + if command['type'] == 'module': + pair = (command, []) + modules.append(pair) + module_name = command.get('name') + if module_name: + module_map[module_name] = pair + elif command['type'] in ('assert_return', 'assert_return_nan', + 'assert_trap'): + if IsValidJSCommand(command): + continue + + action = command['action'] + module_name = action.get('module') + if module_name: + pair = module_map[module_name] + else: + pair = modules[-1] + pair[1].append(command) + return modules + + +class ModuleExtender(object): + def __init__(self, wast2wasm, wasm2wast, temp_dir): + self.wast2wasm = wast2wasm + self.wasm2wast = wasm2wast + self.temp_dir = temp_dir + self.lines = [] + self.exports = {} + + def Extend(self, wasm_path, commands): + wast_path = self._RunWasm2Wast(wasm_path) + with open(wast_path) as wast_file: + wast = wast_file.read() + + self.lines = [] + self.exports = self._GetExports(wast) + for i, command in enumerate(commands): + self._Command(i, command) + + wast = wast[:wast.rindex(')')] + '\n\n' + wast += '\n'.join(self.lines) + ')' + # print wast + with open(wast_path, 'w') as wast_file: + wast_file.write(wast) + return self._RunWast2Wasm(wast_path) + + def _Command(self, index, command): + command_type = command['type'] + new_field = 'assert_%d' % index + if command_type == 'assert_return': + self.lines.append('(func (export "%s")' % new_field) + self.lines.append('block') + self._Action(command['action']) + for expected in command['expected']: + self._Reinterpret(expected['type']) + self._Constant(expected) + self._Reinterpret(expected['type']) + self._Compare(expected['type']) + self.lines.extend(['i32.eqz', 'br_if 0']) + self.lines.extend(['return', 'end', 'unreachable', ')']) + elif command_type == 'assert_return_nan': + type_ = command['expected'][0]['type'] + self.lines.append('(func (export "%s")' % new_field) + self.lines.append('(local %s)' % type_) + self.lines.append('block') + self._Action(command['action']) + self.lines.append('tee_local 0') + self._Reinterpret(type_) + self.lines.append('get_local 0') + self._Reinterpret(type_) + self._Compare(type_) + self.lines.extend(['i32.eqz', 'br_if 0', 'return', 'end', + 'unreachable', ')']) + + # Change the command to assert_return, it won't return NaN anymore. + command['type'] = 'assert_return' + elif command_type == 'assert_trap': + self.lines.append('(func (export "%s")' % new_field) + self._Action(command['action']) + self.lines.extend(['br 0', ')']) + else: + raise Error('Unexpected command: %s' % command_type) + + # Update command to point to the new exported function. + command['action']['field'] = new_field + command['action']['args'] = [] + command['expected'] = [] + + def _GetExports(self, wast): + result = {} + pattern = r'^\s*\(export \"(.*?)\"\s*\((\w+) (\d+)' + for name, type_, index in re.findall(pattern, wast, re.MULTILINE): + result[UnescapeWasmString(name)] = (type_, index) + return result + + def _Action(self, action): + export = self.exports[action['field']] + if action['type'] == 'invoke': + for arg in action['args']: + self._Constant(arg) + self.lines.append('call %s' % export[1]) + elif action['type'] == 'get': + self.lines.append('get_global %s' % export[1]) + else: + raise Error('Unexpected action: %s' % action['type']) + + def _Reinterpret(self, type_): + self.lines.extend({'i32': [], + 'i64': [], + 'f32': ['i32.reinterpret/f32'], + 'f64': ['i64.reinterpret/f64']}[type_]) + + def _Compare(self, type_): + self.lines.append({'i32': 'i32.eq', + 'i64': 'i64.eq', + 'f32': 'i32.eq', + 'f64': 'i64.eq'}[type_]) + + def _Constant(self, const): + inst = None + type_ = const['type'] + if type_ == 'i32': + inst = 'i32.const %s' % const['value'] + elif type_ == 'i64': + inst = 'i64.const %s' % const['value'] + elif type_ == 'f32': + inst = F32ToWasm(int(const['value'])) + elif type_ == 'f64': + inst = F64ToWasm(long(const['value'])) + self.lines.append(inst) + + def _RunWasm2Wast(self, wasm_path): + wast_path = ChangeDir(ChangeExt(wasm_path, '.wast'), self.temp_dir) + self.wasm2wast.RunWithArgs(wasm_path, '-o', wast_path) + return wast_path + + def _RunWast2Wasm(self, wast_path): + wasm_path = ChangeDir(ChangeExt(wast_path, '.wasm'), self.temp_dir) + self.wast2wasm.RunWithArgs(wast_path, '-o', wasm_path) + return wasm_path + + +class JSWriter(object): + def __init__(self, base_dir, commands, out_file): + self.base_dir = base_dir + self.commands = commands + self.out_file = out_file + + def Write(self): + for command in self.commands: + self._WriteCommand(command) + + def _WriteCommand(self, command): + command_funcs = { + 'module': self._WriteModuleCommand, + 'action': self._WriteActionCommand, + 'register': self._WriteRegisterCommand, + 'assert_malformed': self._WriteAssertModuleCommand, + 'assert_invalid': self._WriteAssertModuleCommand, + 'assert_unlinkable': self._WriteAssertModuleCommand, + 'assert_uninstantiable': self._WriteAssertModuleCommand, + 'assert_return': self._WriteAssertReturnCommand, + 'assert_return_nan': self._WriteAssertActionCommand, + 'assert_trap': self._WriteAssertActionCommand, + } + + func = command_funcs.get(command['type']) + if func is None: + raise Error('Unexpected type: %s' % command['type']) + func(command) + + def _WriteModuleCommand(self, command): + if 'name' in command: + self.out_file.write('let %s = ' % command['name']) + self.out_file.write('$$ = instance("%s");\n' % + self._Module(command['filename'])) + + def _WriteActionCommand(self, command): + self.out_file.write('%s;\n' % self._Action(command['action'])) + + def _WriteRegisterCommand(self, command): + self.out_file.write('register("%s", %s)\n' % (command['as'], + command.get('name', '$$'))) + + def _WriteAssertModuleCommand(self, command): + filename = command.get('filename') + if filename: + self.out_file.write('%s("%s");\n' % (command['type'], + self._Module(filename))) + else: + # TODO(binji): this is only needed because assert_invalid doesn't write a + # module file currently. + self.out_file.write('// No filename for command: %s\n' % command) + + def _WriteAssertReturnCommand(self, command): + expected = command['expected'] + if len(expected) == 1: + self.out_file.write('assert_return(() => %s, %s);\n' % + (self._Action(command['action']), + self._ConstantList(expected))) + elif len(expected) == 0: + self._WriteAssertActionCommand(command) + else: + raise Error('Unexpected result with multiple values: %s' % expected) + + def _WriteAssertActionCommand(self, command): + self.out_file.write('%s(() => %s);\n' % (command['type'], + self._Action(command['action']))) + + def _Module(self, filename): + with open(os.path.join(self.base_dir, filename), 'rb') as wasm_file: + return ''.join('\\x%02x' % ord(c) for c in wasm_file.read()) + + def _Constant(self, const): + assert IsValidJSConstant(const), 'Invalid JS const: %s' % const + type_ = const['type'] + value = int(const['value']) + if type_ == 'i32': + return I32ToJS(value) + elif type_ == 'f32': + return F32ToJS(value) + elif type_ == 'f64': + return F64ToJS(value) + else: + assert False + + def _ConstantList(self, consts): + return ', '.join(self._Constant(const) for const in consts) + + def _Action(self, action): + type_ = action['type'] + if type_ not in ('invoke', 'get'): + raise Error('Unexpected action type: %s' % type_) + + args = '' + if type_ == 'invoke': + args = '(%s)' % self._ConstantList(action.get('args', [])) + + return '%s.exports["%s"]%s' % (action.get('module', '$$'), + EscapeJSString(action['field']), + args) def main(args): parser = argparse.ArgumentParser() parser.add_argument('-o', '--output', metavar='PATH', help='output file.') + parser.add_argument('-P', '--prefix', metavar='PATH', help='prefix file.', + default=os.path.join(SCRIPT_DIR, 'gen-spec-prefix.js')) + parser.add_argument('--wast2wasm', metavar='PATH', + help='set the wast2wasm executable to use.') + parser.add_argument('--wasm2wast', metavar='PATH', + help='set the wasm2wast executable to use.') + parser.add_argument('--temp-dir', metavar='PATH', + help='set the directory that temporary wasm/wast' + ' files are written.') + parser.add_argument('--no-error-cmdline', + help='don\'t display the subprocess\'s commandline when' + + ' an error occurs', dest='error_cmdline', + action='store_false') + parser.add_argument('-p', '--print-cmd', + help='print the commands that are run.', + action='store_true') parser.add_argument('file', help='spec json file.') options = parser.parse_args(args) - output_data = ProcessJsonFile(options.file) + wast2wasm = Executable(GetWast2WasmExecutable(options.wast2wasm), + error_cmdline=options.error_cmdline) + wasm2wast = Executable(GetWasm2WastExecutable(options.wasm2wast), + error_cmdline=options.error_cmdline) + + wast2wasm.verbose = options.print_cmd + wasm2wast.verbose = options.print_cmd + + with open(options.file) as json_file: + json_dir = os.path.dirname(options.file) + spec_json = json.load(json_file) + all_commands = spec_json['commands'] + + # modules is a list of pairs: [(module_command, [assert_command, ...]), ...] + modules = CollectInvalidModuleCommands(all_commands) + + with utils.TempDirectory(options.temp_dir, 'gen-spec-js-') as temp_dir: + extender = ModuleExtender(wast2wasm, wasm2wast, temp_dir) + for module_command, assert_commands in modules: + if assert_commands: + wasm_path = os.path.join(json_dir, module_command['filename']) + new_module_filename = extender.Extend(wasm_path, assert_commands) + module_command['filename'] = os.path.relpath(new_module_filename, + json_dir) + + output = cStringIO.StringIO() + if options.prefix: + with open(options.prefix) as prefix_file: + output.write(prefix_file.read()) + output.write('\n') + + JSWriter(json_dir, all_commands, output).Write() + if options.output: - output_file = open(options.output, 'w') + out_file = open(options.output, 'w') else: - output_file = sys.stdout + out_file = sys.stdout try: - output_file.write(output_data) + out_file.write(output.getvalue()) finally: - output_file.close() + out_file.close() return 0 diff --git a/test/gen-spec-js/action.txt b/test/gen-spec-js/action.txt new file mode 100644 index 00000000..ace9edac --- /dev/null +++ b/test/gen-spec-js/action.txt @@ -0,0 +1,17 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (import "spectest" "print" (func (param i32))) + (func (export "print_i32") (param i32) get_local 0 call 0) + + (global (export "global") i32 (i32.const 14))) + +(invoke "print_i32" (i32.const 1)) +(get "global") +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x01\x7f\x00\x02\x12\x01\x08\x73\x70\x65\x63\x74\x65\x73\x74\x05\x70\x72\x69\x6e\x74\x00\x00\x03\x02\x01\x00\x06\x06\x01\x7f\x00\x41\x0e\x0b\x07\x16\x02\x09\x70\x72\x69\x6e\x74\x5f\x69\x33\x32\x00\x01\x06\x67\x6c\x6f\x62\x61\x6c\x03\x00\x0a\x08\x01\x06\x00\x20\x00\x10\x00\x0b"); +$$.exports["print_i32"](1); +$$.exports["global"]; +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_malformed.txt b/test/gen-spec-js/assert_malformed.txt new file mode 100644 index 00000000..c97bb2e9 --- /dev/null +++ b/test/gen-spec-js/assert_malformed.txt @@ -0,0 +1,10 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(assert_malformed + (module "\00asm\bc\0a\00\00") + "unknown binary version") +(;; STDOUT ;;; +// A deliberately empty file for testing. + +assert_malformed("\x00\x61\x73\x6d\xbc\x0a\x00\x00"); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_return.txt b/test/gen-spec-js/assert_return.txt new file mode 100644 index 00000000..02bea616 --- /dev/null +++ b/test/gen-spec-js/assert_return.txt @@ -0,0 +1,48 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (func (export "no_result")) + (func (export "42") (result i32) i32.const 42) + (func (export "i32.add") (param i32 i32) (result i32) + get_local 0 + get_local 1 + i32.add) + (func (export "i64.add") (param i64 i64) (result i64) + get_local 0 + get_local 1 + i64.add) + (func (export "f32.add") (param f32 f32) (result f32) + get_local 0 + get_local 1 + f32.add) + (func (export "f64.add") (param f64 f64) (result f64) + get_local 0 + get_local 1 + f64.add) + (func (export "nan") (result f32) f32.const nan:0x2)) + +(assert_return (invoke "no_result")) +(assert_return (invoke "42") (i32.const 42)) + +(assert_return (invoke "i32.add" (i32.const 1) (i32.const 2)) (i32.const 3)) +;; Rewritten to avoid passing i64 values as parameters. +(assert_return (invoke "i64.add" (i64.const 1) (i64.const 2)) (i64.const 3)) +;; Normal floats are not rewritten. +(assert_return (invoke "f32.add" (f32.const 1) (f32.const 2)) (f32.const 3)) +(assert_return (invoke "f64.add" (f64.const 1) (f64.const 2)) (f64.const 3)) + +;; Rewritten to avoid passing nan as a parameter. +(assert_return (invoke "nan") (f32.const nan:0x2)) + +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x24\x07\x60\x00\x00\x60\x00\x01\x7f\x60\x02\x7f\x7f\x01\x7f\x60\x02\x7e\x7e\x01\x7e\x60\x02\x7d\x7d\x01\x7d\x60\x02\x7c\x7c\x01\x7c\x60\x00\x01\x7d\x03\x0a\x09\x00\x01\x02\x03\x04\x05\x06\x00\x00\x07\x56\x09\x09\x6e\x6f\x5f\x72\x65\x73\x75\x6c\x74\x00\x00\x02\x34\x32\x00\x01\x07\x69\x33\x32\x2e\x61\x64\x64\x00\x02\x07\x69\x36\x34\x2e\x61\x64\x64\x00\x03\x07\x66\x33\x32\x2e\x61\x64\x64\x00\x04\x07\x66\x36\x34\x2e\x61\x64\x64\x00\x05\x03\x6e\x61\x6e\x00\x06\x08\x61\x73\x73\x65\x72\x74\x5f\x30\x00\x07\x08\x61\x73\x73\x65\x72\x74\x5f\x31\x00\x08\x0a\x5a\x09\x02\x00\x0b\x04\x00\x41\x2a\x0b\x07\x00\x20\x00\x20\x01\x6a\x0b\x07\x00\x20\x00\x20\x01\x7c\x0b\x07\x00\x20\x00\x20\x01\x92\x0b\x07\x00\x20\x00\x20\x01\xa0\x0b\x07\x00\x43\x02\x00\x80\x7f\x0b\x13\x00\x02\x40\x42\x01\x42\x02\x10\x03\x42\x03\x51\x45\x0d\x00\x0f\x0b\x00\x0b\x14\x00\x02\x40\x10\x06\xbc\x43\x02\x00\x80\x7f\xbc\x46\x45\x0d\x00\x0f\x0b\x00\x0b"); +assert_return(() => $$.exports["no_result"]()); +assert_return(() => $$.exports["42"](), 42); +assert_return(() => $$.exports["i32.add"](1, 2), 3); +assert_return(() => $$.exports["assert_0"]()); +assert_return(() => $$.exports["f32.add"](f32(1.0), f32(2.0)), f32(3.0)); +assert_return(() => $$.exports["f64.add"](1.0, 2.0), 3.0); +assert_return(() => $$.exports["assert_1"]()); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_return_nan.txt b/test/gen-spec-js/assert_return_nan.txt new file mode 100644 index 00000000..0fdd2334 --- /dev/null +++ b/test/gen-spec-js/assert_return_nan.txt @@ -0,0 +1,16 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (func (export "nan") (result f32) f32.const nan) + (func (export "passthru") (param f32) (result f32) get_local 0)) + +(assert_return_nan (invoke "nan")) +;; Rewritten to avoid passing nan as a parameter. +(assert_return_nan (invoke "passthru" (f32.const -nan))) +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x0d\x03\x60\x00\x01\x7d\x60\x01\x7d\x01\x7d\x60\x00\x00\x03\x04\x03\x00\x01\x02\x07\x1d\x03\x03\x6e\x61\x6e\x00\x00\x08\x70\x61\x73\x73\x74\x68\x72\x75\x00\x01\x08\x61\x73\x73\x65\x72\x74\x5f\x30\x00\x02\x0a\x29\x03\x07\x00\x43\x00\x00\xc0\x7f\x0b\x04\x00\x20\x00\x0b\x1a\x01\x01\x7d\x02\x40\x43\x00\x00\xc0\xff\x10\x01\x22\x00\xbc\x20\x00\xbc\x46\x45\x0d\x00\x0f\x0b\x00\x0b"); +assert_return_nan(() => $$.exports["nan"]()); +assert_return(() => $$.exports["assert_0"]()); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_trap.txt b/test/gen-spec-js/assert_trap.txt new file mode 100644 index 00000000..80a7f632 --- /dev/null +++ b/test/gen-spec-js/assert_trap.txt @@ -0,0 +1,18 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (func (export "unreachable") unreachable) + (func (export "i32.trunc_s") (param f32) (result i32) + get_local 0 + i32.trunc_s/f32)) + +(assert_trap (invoke "unreachable") "unreachable") +;; Rewritten to avoid passing nan as a parameter. +(assert_trap (invoke "i32.trunc_s" (f32.const -nan)) "invalid conversion") +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x09\x02\x60\x00\x00\x60\x01\x7d\x01\x7f\x03\x04\x03\x00\x01\x00\x07\x28\x03\x0b\x75\x6e\x72\x65\x61\x63\x68\x61\x62\x6c\x65\x00\x00\x0b\x69\x33\x32\x2e\x74\x72\x75\x6e\x63\x5f\x73\x00\x01\x08\x61\x73\x73\x65\x72\x74\x5f\x30\x00\x02\x0a\x17\x03\x03\x00\x00\x0b\x05\x00\x20\x00\xa8\x0b\x0b\x00\x43\x00\x00\xc0\xff\x10\x01\x0c\x00\x0b"); +assert_trap(() => $$.exports["unreachable"]()); +assert_trap(() => $$.exports["assert_0"]()); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_uninstantiable.txt b/test/gen-spec-js/assert_uninstantiable.txt new file mode 100644 index 00000000..81cd8bba --- /dev/null +++ b/test/gen-spec-js/assert_uninstantiable.txt @@ -0,0 +1,12 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(assert_trap + (module + (func unreachable) + (start 0)) + "trap in start function") +(;; STDOUT ;;; +// A deliberately empty file for testing. + +assert_uninstantiable("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x04\x01\x60\x00\x00\x03\x02\x01\x00\x08\x01\x00\x0a\x05\x01\x03\x00\x00\x0b"); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/assert_unlinkable.txt b/test/gen-spec-js/assert_unlinkable.txt new file mode 100644 index 00000000..7ec3ce04 --- /dev/null +++ b/test/gen-spec-js/assert_unlinkable.txt @@ -0,0 +1,10 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(assert_unlinkable + (module (import "foo" "bar" (func))) + "module not linkable") +(;; STDOUT ;;; +// A deliberately empty file for testing. + +assert_unlinkable("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x04\x01\x60\x00\x00\x02\x0b\x01\x03\x66\x6f\x6f\x03\x62\x61\x72\x00\x00"); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/basic.txt b/test/gen-spec-js/basic.txt new file mode 100644 index 00000000..85d705ca --- /dev/null +++ b/test/gen-spec-js/basic.txt @@ -0,0 +1,12 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (func (export "42") (result i32) i32.const 42)) + +(assert_return (invoke "42") (i32.const 42)) +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x06\x01\x02\x34\x32\x00\x00\x0a\x06\x01\x04\x00\x41\x2a\x0b"); +assert_return(() => $$.exports["42"](), 42); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/many-modules.txt b/test/gen-spec-js/many-modules.txt new file mode 100644 index 00000000..79c357c5 --- /dev/null +++ b/test/gen-spec-js/many-modules.txt @@ -0,0 +1,21 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module $A (func (export "f") (result i32) i32.const 1)) +(module $B (func (export "f") (result i32) i32.const 2)) +(module $C (func (export "f") (result i32) i32.const 3)) + +(assert_return (invoke "f") (i32.const 3)) +(assert_return (invoke $A "f") (i32.const 1)) +(assert_return (invoke $B "f") (i32.const 2)) +(assert_return (invoke $C "f") (i32.const 3)) +(;; STDOUT ;;; +// A deliberately empty file for testing. + +let $A = $$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x05\x01\x01\x66\x00\x00\x0a\x06\x01\x04\x00\x41\x01\x0b"); +let $B = $$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x05\x01\x01\x66\x00\x00\x0a\x06\x01\x04\x00\x41\x02\x0b"); +let $C = $$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x05\x01\x01\x66\x00\x00\x0a\x06\x01\x04\x00\x41\x03\x0b"); +assert_return(() => $$.exports["f"](), 3); +assert_return(() => $A.exports["f"](), 1); +assert_return(() => $B.exports["f"](), 2); +assert_return(() => $C.exports["f"](), 3); +;;; STDOUT ;;) diff --git a/test/gen-spec-js/register.txt b/test/gen-spec-js/register.txt new file mode 100644 index 00000000..18ffa688 --- /dev/null +++ b/test/gen-spec-js/register.txt @@ -0,0 +1,20 @@ +;;; TOOL: run-gen-spec-js +;;; FLAGS: --prefix=%(test_dir)s/gen-spec-empty-prefix.js +(module + (func (export "f") (result i32) i32.const 1)) + +(register "A") + +(module + (import "A" "f" (func (result i32))) + (func (export "g") (result i32) call 0)) + +(assert_return (invoke "g") (i32.const 1)) +(;; STDOUT ;;; +// A deliberately empty file for testing. + +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x05\x01\x01\x66\x00\x00\x0a\x06\x01\x04\x00\x41\x01\x0b"); +register("A", $$) +$$ = instance("\x00\x61\x73\x6d\x0d\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x02\x07\x01\x01\x41\x01\x66\x00\x00\x03\x02\x01\x00\x07\x05\x01\x01\x67\x00\x01\x0a\x06\x01\x04\x00\x10\x00\x0b"); +assert_return(() => $$.exports["g"](), 1); +;;; STDOUT ;;) diff --git a/test/gen-spec-prefix.js b/test/gen-spec-prefix.js new file mode 100644 index 00000000..99cfab7a --- /dev/null +++ b/test/gen-spec-prefix.js @@ -0,0 +1,93 @@ +/* Copied from * + * https://github.com/WebAssembly/spec/blob/master/interpreter/host/js.ml */ +'use strict'; + +let soft_validate = true; + +let spectest = { + print: print || ((...xs) => console.log(...xs)), + global: 666, + table: new WebAssembly.Table({initial: 10, maximum: 20, element: 'anyfunc'}), + memory: new WebAssembly.Memory({initial: 1, maximum: 2}),}; + +let registry = {spectest}; +let $$; + +function register(name, instance) { + registry[name] = instance.exports; +} + +function module(bytes) { + let buffer = new ArrayBuffer(bytes.length); + let view = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; ++i) { + view[i] = bytes.charCodeAt(i); + } + return new WebAssembly.Module(buffer); +} + +function instance(bytes, imports = registry) { + return new WebAssembly.Instance(module(bytes), imports); +} + +function assert_malformed(bytes) { + try { module(bytes) } catch (e) { + if (e instanceof WebAssembly.CompileError) return; + } + throw new Error("Wasm decoding failure expected"); +} + +function assert_invalid(bytes) { + try { module(bytes) } catch (e) { + if (e instanceof WebAssembly.CompileError) return; + } + throw new Error("Wasm validation failure expected"); +} + +function assert_soft_invalid(bytes) { + try { module(bytes) } catch (e) { + if (e instanceof WebAssembly.CompileError) return; + throw new Error("Wasm validation failure expected"); + } + if (soft_validate) + throw new Error("Wasm validation failure expected"); +} + +function assert_unlinkable(bytes) { + let mod = module(bytes); + try { new WebAssembly.Instance(mod, registry) } catch (e) { + if (e instanceof TypeError) return; + } + throw new Error("Wasm linking failure expected"); +} + +function assert_uninstantiable(bytes) { + let mod = module(bytes); + try { new WebAssembly.Instance(mod, registry) } catch (e) { + if (e instanceof WebAssembly.RuntimeError) return; + } + throw new Error("Wasm trap expected"); +} + +function assert_trap(action) { + try { action() } catch (e) { + if (e instanceof WebAssembly.RuntimeError) return; + } + throw new Error("Wasm trap expected"); +} + +function assert_return(action, expected) { + let actual = action(); + if (!Object.is(actual, expected)) { + throw new Error("Wasm return value " + expected + " expected, got " + actual); + }; +} + +function assert_return_nan(action) { + let actual = action(); + if (!Number.isNaN(actual)) { + throw new Error("Wasm return value NaN expected, got " + actual); + }; +} + +let f32 = Math.fround; diff --git a/test/run-gen-spec-js.py b/test/run-gen-spec-js.py new file mode 100755 index 00000000..73daee12 --- /dev/null +++ b/test/run-gen-spec-js.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# Copyright 2016 WebAssembly Community Group participants +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import argparse +import os +import subprocess +import sys + +import find_exe +import utils +from utils import Error + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +GEN_SPEC_JS_PY = os.path.join(SCRIPT_DIR, 'gen-spec-js.py') + + +def main(args): + parser = argparse.ArgumentParser() + parser.add_argument('-o', '--out-dir', metavar='PATH', + help='output directory for files.') + parser.add_argument('--wast2wasm', metavar='PATH', + help='override wast2wasm executable.') + parser.add_argument('--wasm2wast', metavar='PATH', + help='override wasm2wast executable.') + parser.add_argument('--js-engine', metavar='PATH', + help='the path to the JavaScript engine with which to run' + ' the generated JavaScript. If not specified, JavaScript' + ' output will be written to stdout.') + parser.add_argument('--js-engine-flags', metavar='FLAGS', + help='additional flags for JavaScript engine.', + action='append', default=[]) + parser.add_argument('--prefix-js', + help='Prefix JavaScript file to pass to gen-spec-js') + parser.add_argument('-v', '--verbose', help='print more diagnotic messages.', + action='store_true') + parser.add_argument('--no-error-cmdline', + help='don\'t display the subprocess\'s commandline when' + + ' an error occurs', dest='error_cmdline', + action='store_false') + parser.add_argument('-p', '--print-cmd', help='print the commands that are run.', + action='store_true') + parser.add_argument('--use-libc-allocator', action='store_true') + parser.add_argument('file', help='wast file.') + options = parser.parse_args(args) + + with utils.TempDirectory(options.out_dir, 'run-gen-spec-js-') as out_dir: + wast2wasm = utils.Executable( + find_exe.GetWast2WasmExecutable(options.wast2wasm), + '--spec', + '--no-check-assert-invalid', + error_cmdline=options.error_cmdline) + wast2wasm.AppendOptionalArgs({ + '-v': options.verbose, + '--use-libc-allocator': options.use_libc_allocator + }) + + gen_spec_js = utils.Executable( + sys.executable, GEN_SPEC_JS_PY, + '--temp-dir', out_dir, + error_cmdline=options.error_cmdline) + gen_spec_js.AppendOptionalArgs({ + '--wast2wasm': options.wast2wasm, + '--wasm2wast': options.wasm2wast, + '--prefix': options.prefix_js, + }) + gen_spec_js.verbose = options.print_cmd + + json_file = utils.ChangeDir(utils.ChangeExt(options.file, '.json'), out_dir) + js_file = utils.ChangeExt(json_file, '.js') + wast2wasm.RunWithArgs(options.file, '-o', json_file) + + if options.js_engine: + gen_spec_js.RunWithArgs(json_file, '-o', js_file) + js = utils.Executable(options.js_engine, *options.js_engine_flags) + js.RunWithArgs(js_file) + else: + # Write JavaScript output to stdout + gen_spec_js.RunWithArgs(json_file) + +if __name__ == '__main__': + try: + sys.exit(main(sys.argv[1:])) + except Error as e: + sys.stderr.write(str(e) + '\n') + sys.exit(1) diff --git a/test/run-tests.py b/test/run-tests.py index ba544d98..e0f53d6e 100755 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -157,7 +157,22 @@ TOOLS = { ]), '-v' ] - } + }, + 'run-gen-spec-js': { + 'EXE': 'test/run-gen-spec-js.py', + 'FLAGS': ' '.join([ + '--wast2wasm=%(wast2wasm)s', + '--wasm2wast=%(wasm2wast)s', + '--no-error-cmdline', + '-o', '%(out_dir)s', + ]), + 'VERBOSE-FLAGS': [ + ' '.join([ + '--print-cmd', + ]), + '-v' + ] + }, } ROUNDTRIP_TOOLS = ('wast2wasm',) @@ -743,6 +758,7 @@ def main(args): return 1 variables = {} + variables['test_dir'] = os.path.abspath(SCRIPT_DIR) for exe_basename in find_exe.EXECUTABLES: attr_name = exe_basename.replace('-', '_') diff --git a/test/utils.py b/test/utils.py index 254257cd..49fcdeaf 100644 --- a/test/utils.py +++ b/test/utils.py @@ -99,7 +99,10 @@ class Executable(object): def AppendOptionalArgs(self, option_dict): for option, value in option_dict.items(): if value: - self.AppendArg(option) + if value is True: + self.AppendArg(option) + else: + self.AppendArg('%s=%s' % (option, value)) @contextlib.contextmanager |