From 6553bf72d40a0b9b700c2db72f4de64dcaedeafc Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 09:02:18 -0500 Subject: [PATCH 1/7] Move varargs information into CodeFlags --- Cargo.lock | 1 + bytecode/Cargo.toml | 1 + bytecode/src/bytecode.rs | 47 +++++++++++++++++++++++++++----------- compiler/src/compile.rs | 41 +++++++++++++++++++-------------- vm/src/obj/objcode.rs | 10 ++++++++ vm/src/obj/objfunction.rs | 48 +++++++++++++++++++-------------------- 6 files changed, 94 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f0eced175d..0a30617faaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1534,6 +1534,7 @@ version = "0.1.1" dependencies = [ "bincode", "bitflags", + "itertools", "lz4-compress", "num-bigint", "num-complex", diff --git a/bytecode/Cargo.toml b/bytecode/Cargo.toml index fa72e34515e..e3490e316a6 100644 --- a/bytecode/Cargo.toml +++ b/bytecode/Cargo.toml @@ -15,3 +15,4 @@ lz4-compress = "0.1.1" num-bigint = { version = "0.2", features = ["serde"] } num-complex = { version = "0.2", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } +itertools = "0.8" diff --git a/bytecode/src/bytecode.rs b/bytecode/src/bytecode.rs index 18ae446d901..1db3d3df010 100644 --- a/bytecode/src/bytecode.rs +++ b/bytecode/src/bytecode.rs @@ -2,6 +2,7 @@ //! implements bytecode structure. use bitflags::bitflags; +use itertools::Itertools; use num_bigint::BigInt; use num_complex::Complex64; use serde::{Deserialize, Serialize}; @@ -40,9 +41,9 @@ pub struct CodeObject { pub flags: CodeFlags, pub posonlyarg_count: usize, // Number of positional-only arguments pub arg_names: Vec, // Names of positional arguments - pub varargs: Varargs, // *args or * + pub varargs_name: Option, // *args or * pub kwonlyarg_names: Vec, - pub varkeywords: Varargs, // **kwargs or ** + pub varkeywords_name: Option, // **kwargs or ** pub source_path: String, pub first_line_number: usize, pub obj_name: String, // Name of the object that created this code object @@ -57,6 +58,8 @@ bitflags! { const NEW_LOCALS = 0x08; const IS_GENERATOR = 0x10; const IS_COROUTINE = 0x20; + const HAS_VARARGS = 0x40; + const HAS_VARKEYWORDS = 0x80; } } @@ -70,6 +73,8 @@ impl CodeFlags { pub const NAME_MAPPING: &'static [(&'static str, CodeFlags)] = &[ ("GENERATOR", CodeFlags::IS_GENERATOR), ("COROUTINE", CodeFlags::IS_COROUTINE), + ("VARARGS", CodeFlags::HAS_VARARGS), + ("VARKEYWORDS", CodeFlags::HAS_VARKEYWORDS), ]; } @@ -352,13 +357,6 @@ pub enum UnaryOperator { Plus, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Varargs { - None, - Unnamed, - Named(String), -} - /* Maintain a stack of blocks on the VM. pub enum BlockType { @@ -373,9 +371,9 @@ impl CodeObject { flags: CodeFlags, posonlyarg_count: usize, arg_names: Vec, - varargs: Varargs, + varargs_name: Option, kwonlyarg_names: Vec, - varkeywords: Varargs, + varkeywords_name: Option, source_path: String, first_line_number: usize, obj_name: String, @@ -387,9 +385,9 @@ impl CodeObject { flags, posonlyarg_count, arg_names, - varargs, + varargs_name, kwonlyarg_names, - varkeywords, + varkeywords_name, source_path, first_line_number, obj_name, @@ -418,6 +416,29 @@ impl CodeObject { }) } + pub fn varnames(&self) -> impl Iterator + '_ { + self.arg_names + .iter() + .map(String::as_str) + .chain(self.kwonlyarg_names.iter().map(String::as_str)) + .chain( + self.instructions + .iter() + .filter_map(|i| match i { + Instruction::LoadName { + name, + scope: NameScope::Local, + } + | Instruction::StoreName { + name, + scope: NameScope::Local, + } => Some(name.as_str()), + _ => None, + }) + .unique(), + ) + } + fn display_inner( &self, f: &mut fmt::Formatter, diff --git a/compiler/src/compile.rs b/compiler/src/compile.rs index 8f88965b776..9267c900d5c 100644 --- a/compiler/src/compile.rs +++ b/compiler/src/compile.rs @@ -14,7 +14,7 @@ use crate::symboltable::{ }; use itertools::Itertools; use num_complex::Complex64; -use rustpython_bytecode::bytecode::{self, CallType, CodeObject, Instruction, Label, Varargs}; +use rustpython_bytecode::bytecode::{self, CallType, CodeObject, Instruction, Label}; use rustpython_parser::{ast, parser}; type BasicOutputStream = PeepholeOptimizer; @@ -177,9 +177,9 @@ impl Compiler { Default::default(), 0, Vec::new(), - Varargs::None, + None, Vec::new(), - Varargs::None, + None, self.source_path.clone().unwrap(), line_number, obj_name, @@ -686,14 +686,29 @@ impl Compiler { flags |= bytecode::CodeFlags::HAS_KW_ONLY_DEFAULTS; } + let mut compile_varargs = |va: &ast::Varargs, flag| match va { + ast::Varargs::None => None, + ast::Varargs::Unnamed => { + flags |= flag; + None + } + ast::Varargs::Named(name) => { + flags |= flag; + Some(name.arg.clone()) + } + }; + + let varargs_name = compile_varargs(&args.vararg, bytecode::CodeFlags::HAS_VARARGS); + let varkeywords_name = compile_varargs(&args.kwarg, bytecode::CodeFlags::HAS_VARKEYWORDS); + let line_number = self.get_source_line_number(); self.push_output(CodeObject::new( flags, args.posonlyargs_count, args.args.iter().map(|a| a.arg.clone()).collect(), - compile_varargs(&args.vararg), + varargs_name, args.kwonlyargs.iter().map(|a| a.arg.clone()).collect(), - compile_varargs(&args.kwarg), + varkeywords_name, self.source_path.clone().unwrap(), line_number, name.to_owned(), @@ -970,9 +985,9 @@ impl Compiler { Default::default(), 0, vec![], - Varargs::None, + None, vec![], - Varargs::None, + None, self.source_path.clone().unwrap(), line_number, name.to_owned(), @@ -1920,9 +1935,9 @@ impl Compiler { Default::default(), 1, vec![".0".to_owned()], - Varargs::None, + None, vec![], - Varargs::None, + None, self.source_path.clone().unwrap(), line_number, name.clone(), @@ -2223,14 +2238,6 @@ fn compile_location(location: &ast::Location) -> bytecode::Location { bytecode::Location::new(location.row(), location.column()) } -fn compile_varargs(varargs: &ast::Varargs) -> bytecode::Varargs { - match varargs { - ast::Varargs::None => bytecode::Varargs::None, - ast::Varargs::Unnamed => bytecode::Varargs::Unnamed, - ast::Varargs::Named(param) => bytecode::Varargs::Named(param.arg.clone()), - } -} - fn compile_conversion_flag(conversion_flag: ast::ConversionFlag) -> bytecode::ConversionFlag { match conversion_flag { ast::ConversionFlag::Ascii => bytecode::ConversionFlag::Ascii, diff --git a/vm/src/obj/objcode.rs b/vm/src/obj/objcode.rs index 16410e9be70..d465662430e 100644 --- a/vm/src/obj/objcode.rs +++ b/vm/src/obj/objcode.rs @@ -106,6 +106,16 @@ impl PyCodeRef { fn co_flags(self) -> u8 { self.code.flags.bits() } + + #[pyproperty] + fn co_varnames(self, vm: &VirtualMachine) -> PyObjectRef { + let varnames = self + .code + .varnames() + .map(|s| vm.new_str(s.to_owned())) + .collect(); + vm.ctx.new_tuple(varnames) + } } pub fn init(ctx: &PyContext) { diff --git a/vm/src/obj/objfunction.rs b/vm/src/obj/objfunction.rs index 703c8bfae8d..b485ec7a4cc 100644 --- a/vm/src/obj/objfunction.rs +++ b/vm/src/obj/objfunction.rs @@ -93,37 +93,37 @@ impl PyFunction { } // Pack other positional arguments in to *args: - match code_object.varargs { - bytecode::Varargs::Named(ref vararg_name) => { - let mut last_args = vec![]; - for i in n..nargs { - let arg = &func_args.args[i]; - last_args.push(arg.clone()); - } - let vararg_value = vm.ctx.new_tuple(last_args); - - locals.set_item(vararg_name, vararg_value, vm)?; + if let Some(ref vararg_name) = code_object.varargs_name { + let mut last_args = vec![]; + for i in n..nargs { + let arg = &func_args.args[i]; + last_args.push(arg.clone()); } - bytecode::Varargs::Unnamed | bytecode::Varargs::None => { - // Check the number of positional arguments - if nargs > nexpected_args { - return Err(vm.new_type_error(format!( - "Expected {} arguments (got: {})", - nexpected_args, nargs - ))); - } + let vararg_value = vm.ctx.new_tuple(last_args); + + locals.set_item(vararg_name, vararg_value, vm)?; + } else { + // Check the number of positional arguments + if nargs > nexpected_args { + return Err(vm.new_type_error(format!( + "Expected {} arguments (got: {})", + nexpected_args, nargs + ))); } } // Do we support `**kwargs` ? - let kwargs = match code_object.varkeywords { - bytecode::Varargs::Named(ref kwargs_name) => { - let d = vm.ctx.new_dict(); + let kwargs = if code_object + .flags + .contains(bytecode::CodeFlags::HAS_VARKEYWORDS) + { + let d = vm.ctx.new_dict(); + if let Some(ref kwargs_name) = code_object.varkeywords_name { locals.set_item(kwargs_name, d.as_object().clone(), vm)?; - Some(d) } - bytecode::Varargs::Unnamed => Some(vm.ctx.new_dict()), - bytecode::Varargs::None => None, + Some(d) + } else { + None }; // Handle keyword arguments From a90132377737373f5ec83770e2b94606bf08d4d4 Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 09:02:44 -0500 Subject: [PATCH 2/7] Separate dict.__new__ to dict.__init__ for OrderedDict --- vm/src/obj/objdict.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/vm/src/obj/objdict.rs b/vm/src/obj/objdict.rs index 9bc69c1325d..0573253b94f 100644 --- a/vm/src/obj/objdict.rs +++ b/vm/src/obj/objdict.rs @@ -6,7 +6,7 @@ use super::objstr; use super::objtype::{self, PyClassRef}; use crate::dictdatatype::{self, DictKey}; use crate::exceptions::PyBaseExceptionRef; -use crate::function::{KwArgs, OptionalArg}; +use crate::function::{KwArgs, OptionalArg, PyFuncArgs}; use crate::pyobject::{ IdProtocol, IntoPyObject, ItemProtocol, PyAttributes, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult, PyValue, @@ -41,20 +41,21 @@ impl PyValue for PyDict { #[pyimpl(flags(BASETYPE))] impl PyDictRef { #[pyslot] - fn tp_new( - class: PyClassRef, + fn tp_new(class: PyClassRef, _args: PyFuncArgs, vm: &VirtualMachine) -> PyResult { + PyDict { + entries: RefCell::new(DictContentType::default()), + } + .into_ref_with_type(vm, class) + } + + #[pymethod(magic)] + fn init( + self, dict_obj: OptionalArg, kwargs: KwArgs, vm: &VirtualMachine, - ) -> PyResult { - let dict = DictContentType::default(); - - let entries = RefCell::new(dict); - // it's unfortunate that we can't abstract over RefCall, as we should be able to use dict - // directly here, but that would require generic associated types - PyDictRef::merge(&entries, dict_obj, kwargs, vm)?; - - PyDict { entries }.into_ref_with_type(vm, class) + ) -> PyResult<()> { + PyDictRef::merge(&self.entries, dict_obj, kwargs, vm) } fn merge( From 53a6bc3f5874d734235e0835064f781c4b693ace Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 09:03:55 -0500 Subject: [PATCH 3/7] Add method.__func__ property --- vm/src/obj/objfunction.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/vm/src/obj/objfunction.rs b/vm/src/obj/objfunction.rs index b485ec7a4cc..d76c9609192 100644 --- a/vm/src/obj/objfunction.rs +++ b/vm/src/obj/objfunction.rs @@ -298,9 +298,22 @@ impl PyBoundMethod { )) } + #[pyproperty(magic)] + fn doc(&self, vm: &VirtualMachine) -> PyResult { + vm.get_attribute(self.function.clone(), "__doc__") + } + #[pymethod(magic)] - fn getattribute(&self, name: PyStringRef, vm: &VirtualMachine) -> PyResult { - vm.get_attribute(self.function.clone(), name) + fn getattribute(zelf: PyRef, name: PyStringRef, vm: &VirtualMachine) -> PyResult { + if let Some(obj) = zelf.class().get_attr(name.as_str()) { + return vm.call_if_get_descriptor(obj, zelf.into_object()); + } + vm.get_attribute(zelf.function.clone(), name) + } + + #[pyproperty(magic)] + fn func(&self) -> PyObjectRef { + self.function.clone() } } From dd92e9036655bebd3ed9cfdedbebe1a3b764a32f Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 11:32:36 -0500 Subject: [PATCH 4/7] Add str.__format__ --- vm/src/format.rs | 13 ++++++++++--- vm/src/obj/objstr.rs | 12 +++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/vm/src/format.rs b/vm/src/format.rs index a2bb1ce48f0..c6da20f737f 100644 --- a/vm/src/format.rs +++ b/vm/src/format.rs @@ -422,7 +422,7 @@ impl FormatSpec { } }; - self.format_sign_and_align(magnitude_string, sign_str) + self.format_sign_and_align(&magnitude_string, sign_str) } pub fn format_int(&self, num: &BigInt) -> Result { @@ -486,12 +486,19 @@ impl FormatSpec { }, }; - self.format_sign_and_align(magnitude_string, sign_str) + self.format_sign_and_align(&magnitude_string, sign_str) + } + + pub fn format_string(&self, s: &str) -> Result { + match self.format_type { + Some(FormatType::String) | None => self.format_sign_and_align(s, ""), + _ => Err("Unknown format code for object of type 'str'"), + } } fn format_sign_and_align( &self, - magnitude_string: String, + magnitude_string: &str, sign_str: &str, ) -> Result { let align = self.align.unwrap_or(FormatAlign::Right); diff --git a/vm/src/obj/objstr.rs b/vm/src/obj/objstr.rs index 5a5a5f73c5f..731f7054b8c 100644 --- a/vm/src/obj/objstr.rs +++ b/vm/src/obj/objstr.rs @@ -26,7 +26,7 @@ use crate::cformat::{ CFormatPart, CFormatPreconversor, CFormatQuantity, CFormatSpec, CFormatString, CFormatType, CNumberType, }; -use crate::format::{FormatParseError, FormatPart, FormatPreconversor, FormatString}; +use crate::format::{FormatParseError, FormatPart, FormatPreconversor, FormatSpec, FormatString}; use crate::function::{single_or_tuple_any, OptionalArg, PyFuncArgs}; use crate::pyhash; use crate::pyobject::{ @@ -727,6 +727,16 @@ impl PyString { } } + #[pymethod(name = "__format__")] + fn format_str(&self, spec: PyStringRef, vm: &VirtualMachine) -> PyResult { + match FormatSpec::parse(spec.as_str()) + .and_then(|format_spec| format_spec.format_string(&self.value)) + { + Ok(string) => Ok(string), + Err(err) => Err(vm.new_value_error(err.to_string())), + } + } + /// Return a titlecased version of the string where words start with an /// uppercase character and the remaining characters are lowercase. #[pymethod] From 60d96c42a397cf335f19766085ccd0d7551a3d9b Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 11:36:19 -0500 Subject: [PATCH 5/7] Fix passing a generator to OrderedDict() --- Lib/_collections_abc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 53789e729e9..9e269c88ccf 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -836,7 +836,11 @@ def update(*args, **kwds): len(args)) if args: other = args[0] - if isinstance(other, Mapping): + try: + mapping_inst = isinstance(other, Mapping) + except TypeError: + mapping_inst = False + if mapping_inst: for key in other: self[key] = other[key] elif hasattr(other, "keys"): From 2453b7341750c47bf10404fa7f0185285d6e01ec Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 11:42:52 -0500 Subject: [PATCH 6/7] Mark failing test --- Lib/test/test_unicode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_unicode.py b/Lib/test/test_unicode.py index 5b66361b84c..2c7d15de0dc 100644 --- a/Lib/test/test_unicode.py +++ b/Lib/test/test_unicode.py @@ -2417,6 +2417,8 @@ def test_raiseMemError(self): self.assertRaises(MemoryError, alloc) self.assertRaises(MemoryError, alloc) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_format_subclass(self): class S(str): def __str__(self): From f66fde57c132bd26ab0c4d3b270b6e6fdaae7cf6 Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Thu, 2 Apr 2020 11:58:30 -0500 Subject: [PATCH 7/7] Add json output for CPython tests --- tests/.gitignore | 2 + tests/custom_text_test_runner.py | 613 +++++++++++++++++++++++++++++++ tests/jsontests.py | 17 + 3 files changed, 632 insertions(+) create mode 100644 tests/custom_text_test_runner.py create mode 100644 tests/jsontests.py diff --git a/tests/.gitignore b/tests/.gitignore index 65ff2bbae3b..933f9259efd 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,2 +1,4 @@ snippets/whats_left_*.py .cache/ +cpython_tests_results.json + diff --git a/tests/custom_text_test_runner.py b/tests/custom_text_test_runner.py new file mode 100644 index 00000000000..db2ea4512b9 --- /dev/null +++ b/tests/custom_text_test_runner.py @@ -0,0 +1,613 @@ +#!/usr/bin/env python + +# Modified from https://github.com/agramian/custom-text-test-runner + +# The MIT License (MIT) +# +# Copyright (c) 2015 Abtin Gramian +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import unittest +import os, sys, traceback +import inspect +import json +import time +import re +import operator +from unittest.runner import result +from unittest.runner import registerResult +from functools import reduce + +class TablePrinter(object): + # Modified from https://github.com/agramian/table-printer, same license as above + "Print a list of dicts as a table" + def __init__(self, fmt, sep='', ul=None, tl=None, bl=None): + """ + @param fmt: list of tuple(heading, key, width) + heading: str, column label + key: dictionary key to value to print + width: int, column width in chars + @param sep: string, separation between columns + @param ul: string, character to underline column label, or None for no underlining + @param tl: string, character to draw as top line over table, or None + @param bl: string, character to draw as bottom line under table, or None + """ + super(TablePrinter,self).__init__() + fmt = [x + ('left',) if len(x) < 4 else x for x in fmt] + self.fmt = str(sep).join('{lb}{0}:{align}{1}{rb}'.format(key, width, lb='{', rb='}', align='<' if alignment == 'left' else '>') for heading,key,width,alignment in fmt) + self.head = {key:heading for heading,key,width,alignment in fmt} + self.ul = {key:str(ul)*width for heading,key,width,alignment in fmt} if ul else None + self.width = {key:width for heading,key,width,alignment in fmt} + self.tl = {key:str(tl)*width for heading,key,width,alignment in fmt} if tl else None + self.bl = {key:str(bl)*width for heading,key,width,alignment in fmt} if bl else None + + def row(self, data, separation_character=False): + if separation_character: + return self.fmt.format(**{ k:str(data.get(k,''))[:w] for k,w in self.width.items() }) + else: + data = { k:str(data.get(k,'')) if len(str(data.get(k,''))) <= w else '%s...' %str(data.get(k,''))[:(w-3)] for k,w in self.width.items() } + return self.fmt.format(**data) + + def __call__(self, data_list, totals=None): + _r = self.row + res = [_r(data) for data in data_list] + res.insert(0, _r(self.head)) + if self.ul: + res.insert(1, _r(self.ul, True)) + if self.tl: + res.insert(0, _r(self.tl, True)) + if totals: + if self.ul: + res.insert(len(res), _r(self.ul, True)) + res.insert(len(res), _r(totals)) + if self.bl: + res.insert(len(res), _r(self.bl, True)) + return '\n'.join(res) + + +def get_function_args(func_ref): + try: + return [p for p in inspect.getargspec(func_ref).args if p != 'self'] + except: + return None + +def store_class_fields(class_ref, args_passed): + """ Store the passed in class fields in self + """ + params = get_function_args(class_ref.__init__) + for p in params: setattr(class_ref, p, args_passed[p]) + +def sum_dict_key(d, key, cast_type=None): + """ Sum together all values matching a key given a passed dict + """ + return reduce( (lambda x, y: x + y), [eval("%s(x['%s'])" %(cast_type, key)) if cast_type else x[key] for x in d] ) + +def case_name(name): + """ Test case name decorator to override function name. + """ + def decorator(function): + function.__dict__['test_case_name'] = name + return function + return decorator + +def skip_device(name): + """ Decorator to mark a test to only run on certain devices + Takes single device name or list of names as argument + """ + def decorator(function): + name_list = name if type(name) == list else [name] + function.__dict__['skip_device'] = name_list + return function + return decorator + +def _set_test_type(function, test_type): + """ Test type setter + """ + if 'test_type' in function.__dict__: + function.__dict__['test_type'].append(test_type) + else: + function.__dict__['test_type'] = [test_type] + return function + +def smoke(function): + """ Test decorator to mark test as smoke type + """ + return _set_test_type(function, 'smoke') + +def guide_discovery(function): + """ Test decorator to mark test as guide_discovery type + """ + return _set_test_type(function, 'guide_discovery') + +def focus(function): + """ Test decorator to mark test as focus type to all rspec style debugging of cases + """ + return _set_test_type(function, 'focus') + +class _WritelnDecorator(object): + """Used to decorate file-like objects with a handy 'writeln' method""" + def __init__(self,stream): + self.stream = stream + + def __getattr__(self, attr): + if attr in ('stream', '__getstate__'): + raise AttributeError(attr) + return getattr(self.stream,attr) + + def writeln(self, arg=None): + if arg: + self.write(arg) + self.write('\n') # text-mode streams translate to \r\n if needed + +class CustomTextTestResult(result.TestResult): + _num_formatting_chars = 150 + _execution_time_significant_digits = 4 + _pass_percentage_significant_digits = 2 + + def __init__(self, stream, descriptions, verbosity, results_file_path, result_screenshots_dir, show_previous_results, config, test_types): + super(CustomTextTestResult, self).__init__(stream, descriptions, verbosity) + store_class_fields(self, locals()) + self.show_overall_results = verbosity > 0 + self.show_test_info = verbosity > 1 + self.show_individual_suite_results = verbosity > 2 + self.show_errors = verbosity > 3 + self.show_errors_detail = verbosity > 4 + self.show_all = verbosity > 4 + self.suite = None + self.total_execution_time = 0 + self.separator1 = "=" * CustomTextTestResult._num_formatting_chars + self.separator2 = "-" * CustomTextTestResult._num_formatting_chars + self.separator3 = "_" * CustomTextTestResult._num_formatting_chars + self.separator4 = "*" * CustomTextTestResult._num_formatting_chars + self.separator_failure = "!" * CustomTextTestResult._num_formatting_chars + self.separator_pre_result = '.' * CustomTextTestResult._num_formatting_chars + + def getDescription(self, test): + doc_first_line = test.shortDescription() + if self.descriptions and doc_first_line: + return '\n'.join((str(test), doc_first_line)) + else: + return str(test) + + def getSuiteDescription(self, test): + doc = test.__class__.__doc__ + return doc and doc.split("\n")[0].strip() or None + + def startTestRun(self): + self.results = None + self.previous_suite_runs = [] + if os.path.isfile(self.results_file_path): + with open(self.results_file_path, 'rb') as f: + try: + self.results = json.load(f) + # recreated results dict with int keys + self.results['suites'] = {int(k):v for (k,v) in list(self.results['suites'].items())} + self.suite_map = {v['name']:int(k) for (k,v) in list(self.results['suites'].items())} + self.previous_suite_runs = list(self.results['suites'].keys()) + except: + pass + if not self.results: + self.results = {'suites': {}, + 'name': '', + 'num_passed': 0, + 'num_failed': 0, + 'num_skipped': 0, + 'execution_time': None} + self.suite_number = int(sorted(self.results['suites'].keys())[-1]) + 1 if len(self.results['suites']) else 0 + self.case_number = 0 + self.suite_map = {} + + def stopTestRun(self): + # if no tests or some failure occured execution time may not have been set + try: + self.results['suites'][self.suite_map[self.suite]]['execution_time'] = format(self.suite_execution_time, '.%sf' %CustomTextTestResult._execution_time_significant_digits) + except: + pass + self.results['execution_time'] = format(self.total_execution_time, '.%sf' %CustomTextTestResult._execution_time_significant_digits) + self.stream.writeln(self.separator3) + with open(self.results_file_path, 'w') as f: + json.dump(self.results, f) + + def startTest(self, test): + suite_base_category = test.__class__.base_test_category if hasattr(test.__class__, 'base_test_category') else '' + self.next_suite = os.path.join(suite_base_category, test.__class__.name if hasattr(test.__class__, 'name') else test.__class__.__name__) + self.case = test._testMethodName + super(CustomTextTestResult, self).startTest(test) + if not self.suite or self.suite != self.next_suite: + if self.suite: + self.results['suites'][self.suite_map[self.suite]]['execution_time'] = format(self.suite_execution_time, '.%sf' %CustomTextTestResult._execution_time_significant_digits) + self.suite_execution_time = 0 + self.suite = self.next_suite + if self.show_test_info: + self.stream.writeln(self.separator1) + self.stream.writeln("TEST SUITE: %s" %self.suite) + self.stream.writeln("Description: %s" %self.getSuiteDescription(test)) + try: + name_override = getattr(test, test._testMethodName).__func__.__dict__['test_case_name'] + except: + name_override = None + self.case = name_override if name_override else self.case + if self.show_test_info: + self.stream.writeln(self.separator2) + self.stream.writeln("CASE: %s" %self.case) + self.stream.writeln("Description: %s" %test.shortDescription()) + self.stream.writeln(self.separator2) + self.stream.flush() + self.current_case_number = self.case_number + if self.suite not in self.suite_map: + self.suite_map[self.suite] = self.suite_number + self.results['suites'][self.suite_number] = { + 'name': self.suite, + 'class': test.__class__.__name__, + 'module': re.compile('.* \((.*)\)').match(str(test)).group(1), + 'description': self.getSuiteDescription(test), + 'cases': {}, + 'used_case_names': {}, + 'num_passed': 0, + 'num_failed': 0, + 'num_skipped': 0, + 'execution_time': None} + self.suite_number += 1 + self.num_cases = 0 + self.num_passed = 0 + self.num_failed = 0 + self.num_skipped = 0 + self.results['suites'][self.suite_map[self.suite]]['cases'][self.case_number] = { + 'name': self.case, + 'method': test._testMethodName, + 'result': None, + 'description': test.shortDescription(), + 'note': None, + 'errors': None, + 'failures': None, + 'screenshots': [], + 'new_version': 'No', + 'execution_time': None} + self.start_time = time.time() + if self.test_types: + if ('test_type' in getattr(test, test._testMethodName).__func__.__dict__ + and set([s.lower() for s in self.test_types]) == set([s.lower() for s in getattr(test, test._testMethodName).__func__.__dict__['test_type']])): + pass + else: + getattr(test, test._testMethodName).__func__.__dict__['__unittest_skip_why__'] = 'Test run specified to only run tests of type "%s"' %','.join(self.test_types) + getattr(test, test._testMethodName).__func__.__dict__['__unittest_skip__'] = True + if 'skip_device' in getattr(test, test._testMethodName).__func__.__dict__: + for device in getattr(test, test._testMethodName).__func__.__dict__['skip_device']: + if self.config and device.lower() in self.config['device_name'].lower(): + getattr(test, test._testMethodName).__func__.__dict__['__unittest_skip_why__'] = 'Test is marked to be skipped on %s' %device + getattr(test, test._testMethodName).__func__.__dict__['__unittest_skip__'] = True + break + + def stopTest(self, test): + self.end_time = time.time() + self.execution_time = self.end_time - self.start_time + self.suite_execution_time += self.execution_time + self.total_execution_time += self.execution_time + super(CustomTextTestResult, self).stopTest(test) + self.num_cases += 1 + self.results['suites'][self.suite_map[self.suite]]['num_passed'] = self.num_passed + self.results['suites'][self.suite_map[self.suite]]['num_failed'] = self.num_failed + self.results['suites'][self.suite_map[self.suite]]['num_skipped'] = self.num_skipped + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['execution_time']= format(self.execution_time, '.%sf' %CustomTextTestResult._execution_time_significant_digits) + self.results['num_passed'] += self.num_passed + self.results['num_failed'] += self.num_failed + self.results['num_skipped'] += self.num_skipped + self.case_number += 1 + + def print_error_string(self, err): + error_string = ''.join(traceback.format_exception(err[0], err[1], err[2])) + if self.show_errors: + self.stream.writeln(self.separator_failure) + self.stream.write(error_string) + return error_string + + def addScreenshots(self, test): + for root, dirs, files in os.walk(self.result_screenshots_dir): + for file in files: + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['screenshots'].append(os.path.join(root, file)) + + def addSuccess(self, test): + super(CustomTextTestResult, self).addSuccess(test) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("PASS") + self.stream.flush() + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['result'] = 'passed' + self.num_passed += 1 + self.addScreenshots(test) + + def addError(self, test, err): + error_string = self.print_error_string(err) + super(CustomTextTestResult, self).addError(test, err) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("ERROR") + self.stream.flush() + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['result'] = 'error' + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['errors'] = error_string + self.num_failed += 1 + self.addScreenshots(test) + + def addFailure(self, test, err): + error_string = self.print_error_string(err) + super(CustomTextTestResult, self).addFailure(test, err) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("FAIL") + self.stream.flush() + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['result'] = 'failed' + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['failures'] = error_string + self.num_failed += 1 + self.addScreenshots(test) + + def addSkip(self, test, reason): + super(CustomTextTestResult, self).addSkip(test, reason) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("SKIPPED {0!r}".format(reason)) + self.stream.flush() + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['result'] = 'skipped' + self.results['suites'][self.suite_map[self.suite]]['cases'][self.current_case_number]['note'] = getattr(getattr(test, test._testMethodName), "__unittest_skip_why__", reason) + self.num_skipped += 1 + + def addExpectedFailure(self, test, err): + super(CustomTextTestResult, self).addExpectedFailure(test, err) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("EXPECTED FAILURE") + self.stream.flush() + self.num_passed += 1 + self.addScreenshots(test) + + def addUnexpectedSuccess(self, test): + super(CustomTextTestResult, self).addUnexpectedSuccess(test) + if self.show_test_info: + self.stream.writeln(self.separator_pre_result) + self.stream.writeln("UNEXPECTED SUCCESS") + self.stream.flush() + self.num_failed += 1 + self.addScreenshots(test) + + def printOverallSuiteResults(self, r): + self.stream.writeln() + self.stream.writeln(self.separator4) + self.stream.writeln('OVERALL SUITE RESULTS') + fmt = [ + ('SUITE', 'suite', 50, 'left'), + ('CASES', 'cases', 15, 'right'), + ('PASSED', 'passed', 15, 'right'), + ('FAILED', 'failed', 15, 'right'), + ('SKIPPED', 'skipped', 15, 'right'), + ('%', 'percentage', 20, 'right'), + ('TIME (s)', 'time', 20, 'right') + ] + data = [] + for x in r: data.append({'suite': r[x]['name'], + 'cases': r[x]['num_passed'] + r[x]['num_failed'], + 'passed': r[x]['num_passed'], + 'failed': r[x]['num_failed'], + 'skipped': r[x]['num_skipped'], + 'percentage': float(r[x]['num_passed'])/(r[x]['num_passed'] + r[x]['num_failed']) * 100 if (r[x]['num_passed'] + r[x]['num_failed']) > 0 else 0, + 'time': r[x]['execution_time']}) + total_suites_passed = len([x for x in data if not x['failed']]) + total_suites_passed_percentage = format(float(total_suites_passed)/len(data) * 100, '.%sf' %CustomTextTestResult._pass_percentage_significant_digits) + totals = {'suite': 'TOTALS %s/%s (%s%%) suites passed' %(total_suites_passed, len(data), total_suites_passed_percentage), + 'cases': sum_dict_key(data, 'cases'), + 'passed': sum_dict_key(data, 'passed'), + 'failed': sum_dict_key(data, 'failed'), + 'skipped': sum_dict_key(data, 'skipped'), + 'percentage': sum_dict_key(data, 'percentage')/len(data), + 'time': sum_dict_key(data, 'time', 'float')} + for x in data: operator.setitem(x, 'percentage', format(x['percentage'], '.%sf' %CustomTextTestResult._pass_percentage_significant_digits)) + totals['percentage'] = format(totals['percentage'], '.%sf' %CustomTextTestResult._pass_percentage_significant_digits) + self.stream.writeln( TablePrinter(fmt, tl=self.separator1, ul=self.separator2, bl=self.separator3)(data, totals) ) + self.stream.writeln() + + def printIndividualSuiteResults(self, r): + self.stream.writeln() + self.stream.writeln(self.separator4) + self.stream.writeln('INDIVIDUAL SUITE RESULTS') + fmt = [ + ('CASE', 'case', 50, 'left'), + ('DESCRIPTION', 'description', 50, 'right'), + ('RESULT', 'result', 25, 'right'), + ('TIME (s)', 'time', 25, 'right') + ] + for suite in r: + self.stream.writeln(self.separator1) + self.stream.write('{0: <50}'.format('SUITE: %s' %r[suite]['name'])) + self.stream.writeln('{0: <100}'.format('DESCRIPTION: %s' %(r[suite]['description'] if not r[suite]['description'] or len(r[suite]['description']) <= (100 - len('DESCRIPTION: ')) + else '%s...' %r[suite]['description'][:(97 - len('DESCRIPTION: '))]))) + data = [] + cases = r[suite]['cases'] + for x in cases: data.append({'case': cases[x]['name'], + 'description': cases[x]['description'], + 'result': cases[x]['result'].upper() if cases[x]['result'] else cases[x]['result'], + 'time': cases[x]['execution_time']}) + self.stream.writeln( TablePrinter(fmt, tl=self.separator1, ul=self.separator2)(data) ) + self.stream.writeln(self.separator3) + self.stream.writeln() + + def printErrorsOverview(self, r): + self.stream.writeln() + self.stream.writeln(self.separator4) + self.stream.writeln('FAILURES AND ERRORS OVERVIEW') + fmt = [ + ('SUITE', 'suite', 50, 'left'), + ('CASE', 'case', 50, 'left'), + ('RESULT', 'result', 50, 'right') + ] + data = [] + for suite in r: + cases = {k:v for (k,v) in list(r[suite]['cases'].items()) if v['failures'] or v['errors']} + for x in cases: data.append({'suite': '%s%s' %(r[suite]['name'], ' (%s)' %r[suite]['module'] if r[suite]['class'] != r[suite]['name'] else ''), + 'case': '%s%s' %(cases[x]['name'], ' (%s)' %cases[x]['method'] if cases[x]['name'] != cases[x]['method'] else ''), + 'result': cases[x]['result'].upper()}) + self.stream.writeln( TablePrinter(fmt, tl=self.separator1, ul=self.separator2)(data) ) + self.stream.writeln(self.separator3) + self.stream.writeln() + + def printErrorsDetail(self, r): + self.stream.writeln() + self.stream.writeln(self.separator4) + self.stream.writeln('FAILURES AND ERRORS DETAIL') + for suite in r: + failures_and_errors = [k for (k,v) in list(r[suite]['cases'].items()) if v['failures'] or v['errors']] + #print failures_and_errors + suite_str = '%s%s' %(r[suite]['name'], ' (%s)' %r[suite]['module'] if r[suite]['class'] != r[suite]['name'] else '') + for case in failures_and_errors: + case_ref = r[suite]['cases'][case] + case_str = '%s%s' %(case_ref['name'], ' (%s)' %case_ref['method'] if case_ref['name'] != case_ref['method'] else '') + errors = case_ref['errors'] + failures = case_ref['failures'] + self.stream.writeln(self.separator1) + if errors: + self.stream.writeln('ERROR: %s [%s]' %(case_str, suite_str)) + self.stream.writeln(self.separator2) + self.stream.writeln(errors) + if failures: + self.stream.writeln('FAILURE: %s [%s]' %(case_str, suite_str)) + self.stream.writeln(self.separator2) + self.stream.writeln(failures) + self.stream.writeln(self.separator3) + self.stream.writeln() + + def printSkippedDetail(self, r): + self.stream.writeln() + self.stream.writeln(self.separator4) + self.stream.writeln('SKIPPED DETAIL') + fmt = [ + ('SUITE', 'suite', 50, 'left'), + ('CASE', 'case', 50, 'left'), + ('REASON', 'reason', 50, 'right') + ] + data = [] + for suite in r: + cases = {k:v for (k,v) in list(r[suite]['cases'].items()) if v['result'] == 'skipped'} + for x in cases: data.append({'suite': '%s%s' %(r[suite]['name'], ' (%s)' %r[suite]['module'] if r[suite]['class'] != r[suite]['name'] else ''), + 'case': '%s%s' %(cases[x]['name'], ' (%s)' %cases[x]['method'] if cases[x]['name'] != cases[x]['method'] else ''), + 'reason': cases[x]['note']}) + self.stream.writeln( TablePrinter(fmt, tl=self.separator1, ul=self.separator2)(data) ) + self.stream.writeln(self.separator3) + self.stream.writeln() + + def returnCode(self): + return not self.wasSuccessful() + +class CustomTextTestRunner(unittest.TextTestRunner): + """A test runner class that displays results in textual form. + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + + def __init__(self, + stream=sys.stderr, + descriptions=True, + verbosity=1, + failfast=False, + buffer=False, + resultclass=CustomTextTestResult, + results_file_path="results.json", + result_screenshots_dir='', + show_previous_results=False, + test_name=None, + test_description=None, + config=None, + test_types=None): + store_class_fields(self, locals()) + self.stream = _WritelnDecorator(stream) + + def _makeResult(self): + return self.resultclass(self.stream, self.descriptions, self.verbosity, + self.results_file_path, self.result_screenshots_dir, self.show_previous_results, + self.config, self.test_types) + + def run(self, test): + output = "" + "Run the given test case or test suite." + result = self._makeResult() + registerResult(result) + result.failfast = self.failfast + result.buffer = self.buffer + startTime = time.time() + startTestRun = getattr(result, 'startTestRun', None) + if startTestRun is not None: + startTestRun() + try: + test(result) + finally: + stopTestRun = getattr(result, 'stopTestRun', None) + if stopTestRun is not None: + stopTestRun() + stopTime = time.time() + timeTaken = stopTime - startTime + # filter results to output + if result.show_previous_results: + r = result.results['suites'] + else: + r = {k:v for (k,v) in list(result.results['suites'].items()) if k not in result.previous_suite_runs} + # print results based on verbosity + if result.show_all: + result.printSkippedDetail(r) + if result.show_errors_detail: + result.printErrorsDetail(r) + if result.show_individual_suite_results: + result.printIndividualSuiteResults(r) + if result.show_errors: + result.printErrorsOverview(r) + if result.show_overall_results: + result.printOverallSuiteResults(r) + run = result.testsRun + self.stream.writeln("Ran %d test case%s in %.4fs" % + (run, run != 1 and "s" or "", timeTaken)) + self.stream.writeln() + + expectedFails = unexpectedSuccesses = skipped = 0 + try: + results = map(len, (result.expectedFailures, + result.unexpectedSuccesses, + result.skipped)) + except AttributeError: + pass + else: + expectedFails, unexpectedSuccesses, skipped = results + + infos = [] + if not result.wasSuccessful(): + self.stream.write("FAILED") + failed, errored = map(len, (result.failures, result.errors)) + if failed: + infos.append("failures=%d" % failed) + if errored: + infos.append("errors=%d" % errored) + else: + self.stream.write("OK") + if skipped: + infos.append("skipped=%d" % skipped) + if expectedFails: + infos.append("expected failures=%d" % expectedFails) + if unexpectedSuccesses: + infos.append("unexpected successes=%d" % unexpectedSuccesses) + if infos: + self.stream.writeln(" (%s)" % (", ".join(infos),)) + else: + self.stream.write("\n") + return result diff --git a/tests/jsontests.py b/tests/jsontests.py new file mode 100644 index 00000000000..6777299b663 --- /dev/null +++ b/tests/jsontests.py @@ -0,0 +1,17 @@ +import unittest +from custom_text_test_runner import CustomTextTestRunner as Runner +from test.libregrtest.runtest import findtests +import os + +testnames = ('test.' + name for name in findtests()) + +suite = unittest.defaultTestLoader.loadTestsFromNames(testnames) + +resultsfile = os.path.join(os.path.dirname(__file__), "cpython_tests_results.json") +if os.path.exists(resultsfile): + os.remove(resultsfile) + +runner = Runner(results_file_path=resultsfile) +runner.run(suite) + +print("Done! results are available in", resultsfile)