Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

WIP: Fix CPython tests #54

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test: dev-packages install quicktest ## Intall hyperjson module and run tests

.PHONY: quicktest
quicktest: ## Run tests on already installed hyperjson module
pipenv run pytest tests
pipenv run pytest tests/cpython/test_fail.py

.PHONY: bench
bench: ## Run benchmarks
Expand Down
2 changes: 1 addition & 1 deletion package/hyperjson/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .hyperjson import loads, load, dumps, dump, __version__
from .hyperjson import *
38 changes: 26 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,32 @@ pub enum HyperJsonError {
InvalidFloat { x: String },
#[fail(display = "Invalid type: {}, Error: {}", t, e)]
InvalidCast { t: String, e: String },
#[fail(display = "Extra data")]
TrailingCharacters { line: usize, column: usize },
// NoneError doesn't have an impl for `Display`
// See https://github.com/rust-lang-nursery/failure/issues/61
// See https://github.com/rust-lang/rust/issues/42327#issuecomment-378324282
// #[fail(display = "Error: {}", s)]
// NoneError { s: String },
// NoneError { s: String }
}

impl From<serde_json::Error> for HyperJsonError {
fn from(error: serde_json::Error) -> HyperJsonError {
let msg = error.to_string();

if msg.contains("trailing characters") {
return HyperJsonError::TrailingCharacters {
line: error.line(),
column: error.column(),
};
}
if msg.contains("") {
return HyperJsonError::TrailingCharacters {
line: error.line(),
column: error.column(),
};
}

HyperJsonError::InvalidConversion { error }
}
}
Expand All @@ -51,6 +68,9 @@ impl From<HyperJsonError> for PyErr {
HyperJsonError::InvalidConversion { error } => {
PyErr::new::<PyTypeError, _>(format!("{}", error))
}
HyperJsonError::TrailingCharacters { line, column } => {
JSONDecodeError::py_err((h.to_string(), "".to_string(), column))
}
// TODO
HyperJsonError::PyErr { error: _error } => PyErr::new::<PyTypeError, _>("PyErr"),
HyperJsonError::InvalidCast { t: _t, e: _e } => {
Expand All @@ -71,8 +91,6 @@ impl From<PyErr> for HyperJsonError {
}
}

import_exception!(json, JSONDecodeError);

#[pyfunction]
pub fn load(py: Python, fp: PyObject, kwargs: Option<&PyDict>) -> PyResult<PyObject> {
// Temporary workaround for
Expand Down Expand Up @@ -205,15 +223,13 @@ pub fn dump(
Ok(pyo3::Python::None(py))
}

py_exception!(hyperjson, JSONDecodeError, pyo3::exceptions::Exception);

/// A hyper-fast JSON encoder/decoder written in Rust
#[pymodinit]
fn hyperjson(_py: Python, m: &PyModule) -> PyResult<()> {
// See https://github.com/PyO3/pyo3/issues/171
// Use JSONDecodeError from stdlib until issue is resolved.
// py_exception!(_hyperjson, JSONDecodeError);
// m.add("JSONDecodeError", py.get_type::<JSONDecodeError>());

fn hyperjson(py: Python, m: &PyModule) -> PyResult<()> {
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("JSONDecodeError", py.get_type::<JSONDecodeError>())?;

m.add_function(wrap_function!(load))?;
m.add_function(wrap_function!(loads))?;
Expand All @@ -240,9 +256,7 @@ pub fn loads_impl(
let seed = HyperJsonValue::new(py, &parse_float, &parse_int);
match seed.deserialize(&mut deserializer) {
Ok(py_object) => {
deserializer
.end()
.map_err(|e| JSONDecodeError::py_err((e.to_string(), string.clone(), 0)))?;
deserializer.end().map_err(HyperJsonError::from)?;
Ok(py_object)
}
Err(e) => {
Expand Down
83 changes: 83 additions & 0 deletions tests/cpython/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import os
import json
import doctest
import unittest

from test import support

# import json with and without accelerations
cjson = support.import_fresh_module('json', fresh=['_json'])
pyjson = support.import_fresh_module('json', blocked=['_json'])
hyperjson = support.import_fresh_module('hyperjson')
# JSONDecodeError is cached inside the _json module
cjson.JSONDecodeError = cjson.decoder.JSONDecodeError = json.JSONDecodeError

# create two base classes that will be used by the other tests


class PyTest(unittest.TestCase):
json = pyjson
loads = staticmethod(pyjson.loads)
dumps = staticmethod(pyjson.dumps)
JSONDecodeError = staticmethod(pyjson.JSONDecodeError)


@unittest.skipUnless(cjson, 'requires _json')
class CTest(unittest.TestCase):
if cjson is not None:
json = cjson
loads = staticmethod(cjson.loads)
dumps = staticmethod(cjson.dumps)
JSONDecodeError = staticmethod(cjson.JSONDecodeError)


@unittest.skipUnless(hyperjson, 'requires hyperjson')
class RustTest(unittest.TestCase):
if hyperjson is not None:
json = hyperjson
loads = staticmethod(hyperjson.loads)
dumps = staticmethod(hyperjson.dumps)
JSONDecodeError = staticmethod(hyperjson.JSONDecodeError)

# test PyTest and CTest checking if the functions come from the right module


class TestPyTest(PyTest):
def test_pyjson(self):
self.assertEqual(self.json.scanner.make_scanner.__module__,
'json.scanner')
self.assertEqual(self.json.decoder.scanstring.__module__,
'json.decoder')
self.assertEqual(self.json.encoder.encode_basestring_ascii.__module__,
'json.encoder')


class TestCTest(CTest):
def test_cjson(self):
self.assertEqual(self.json.scanner.make_scanner.__module__, '_json')
self.assertEqual(self.json.decoder.scanstring.__module__, '_json')
self.assertEqual(self.json.encoder.c_make_encoder.__module__, '_json')
self.assertEqual(self.json.encoder.encode_basestring_ascii.__module__,
'_json')


class TestRustTest(RustTest):
def test_hyperjson(self):
# FIXME: hyperjson does not have any of this
pass
# self.assertEqual(self.json.scanner.make_scanner.__module__, 'hyperjson')
# self.assertEqual(self.json.decoder.scanstring.__module__, 'hyperjson')
# self.assertEqual(self.json.encoder.c_make_encoder.__module__, 'hyperjson')
# self.assertEqual(self.json.encoder.encode_basestring_ascii.__module__,
# 'hyperjson')


def load_tests(loader, _, pattern):
suite = unittest.TestSuite()
for mod in (json, json.encoder, json.decoder):
suite.addTest(doctest.DocTestSuite(mod))
suite.addTest(TestPyTest('test_pyjson'))
suite.addTest(TestCTest('test_cjson'))

pkg_dir = os.path.dirname(__file__)
return support.load_package_tests(pkg_dir, loader, suite, pattern)
4 changes: 4 additions & 0 deletions tests/cpython/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import unittest
from cpython import load_tests

unittest.main()
102 changes: 102 additions & 0 deletions tests/cpython/test_decode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import decimal
from io import StringIO, BytesIO
from collections import OrderedDict
from cpython import PyTest, CTest, RustTest
import pytest


class _TestDecode:
def test_decimal(self):
rval = self.loads('1.1', parse_float=decimal.Decimal)
self.assertTrue(isinstance(rval, decimal.Decimal))
self.assertEqual(rval, decimal.Decimal('1.1'))

def test_float(self):
rval = self.loads('1', parse_int=float)
self.assertTrue(isinstance(rval, float))
self.assertEqual(rval, 1.0)

def test_empty_objects(self):
self.assertEqual(self.loads('{}'), {})
self.assertEqual(self.loads('[]'), [])
self.assertEqual(self.loads('""'), "")

@pytest.mark.skip(reason="TypeError: 'object_pairs_hook' is an invalid keyword argument for this function")
def test_object_pairs_hook(self):
s = '{"xkd":1, "kcw":2, "art":3, "hxm":4, "qrt":5, "pad":6, "hoy":7}'
p = [("xkd", 1), ("kcw", 2), ("art", 3), ("hxm", 4),
("qrt", 5), ("pad", 6), ("hoy", 7)]
self.assertEqual(self.loads(s), eval(s))
self.assertEqual(self.loads(s, object_pairs_hook=lambda x: x), p)
self.assertEqual(self.json.load(StringIO(s),
object_pairs_hook=lambda x: x), p)
od = self.loads(s, object_pairs_hook=OrderedDict)
self.assertEqual(od, OrderedDict(p))
self.assertEqual(type(od), OrderedDict)
# the object_pairs_hook takes priority over the object_hook
self.assertEqual(self.loads(s, object_pairs_hook=OrderedDict,
object_hook=lambda x: None),
OrderedDict(p))
# check that empty object literals work (see #17368)
self.assertEqual(self.loads('{}', object_pairs_hook=OrderedDict),
OrderedDict())
self.assertEqual(self.loads('{"empty": {}}',
object_pairs_hook=OrderedDict),
OrderedDict([('empty', OrderedDict())]))

def test_decoder_optimizations(self):
# Several optimizations were made that skip over calls to
# the whitespace regex, so this test is designed to try and
# exercise the uncommon cases. The array cases are already covered.
rval = self.loads('{ "key" : "value" , "k":"v" }')
self.assertEqual(rval, {"key": "value", "k": "v"})

def check_keys_reuse(self, source, loads):
rval = loads(source)
(a, b), (c, d) = sorted(rval[0]), sorted(rval[1])
self.assertIs(a, c)
self.assertIs(b, d)

def test_keys_reuse(self):
s = '[{"a_key": 1, "b_\xe9": 2}, {"a_key": 3, "b_\xe9": 4}]'
self.check_keys_reuse(s, self.loads)
self.check_keys_reuse(s, self.json.decoder.JSONDecoder().decode)

def test_extra_data(self):
s = '[1, 2, 3]5'
msg = 'Extra data'
self.assertRaisesRegex(self.JSONDecodeError, msg, self.loads, s)

def test_invalid_escape(self):
s = '["abc\\y"]'
msg = 'escape'
self.assertRaisesRegex(self.JSONDecodeError, msg, self.loads, s)

def test_invalid_input_type(self):
msg = 'the JSON object must be str'
for value in [1, 3.14, [], {}, None]:
self.assertRaisesRegex(TypeError, msg, self.loads, value)

@pytest.mark.skip(reason="AssertionError: 'BOM' not found in Value ufeff[1,2,3]', 0))")
def test_string_with_utf8_bom(self):
# see #18958
bom_json = "[1,2,3]".encode('utf-8-sig').decode('utf-8')
with self.assertRaises(self.JSONDecodeError) as cm:
self.loads(bom_json)
self.assertIn('BOM', str(cm.exception))
with self.assertRaises(self.JSONDecodeError) as cm:
self.json.load(StringIO(bom_json))
self.assertIn('BOM', str(cm.exception))
# make sure that the BOM is not detected in the middle of a string
bom_in_str = '"{}"'.format(''.encode('utf-8-sig').decode('utf-8'))
self.assertEqual(self.loads(bom_in_str), '\ufeff')
self.assertEqual(self.json.load(StringIO(bom_in_str)), '\ufeff')

@pytest.mark.skip(reason="AttributeError: module 'hyperjson' has no attribute 'JSONDecoder'")
def test_negative_index(self):
d = self.json.JSONDecoder()
self.assertRaises(ValueError, d.raw_decode, 'a'*42, -50000)


class TestRustDecode(_TestDecode, RustTest):
pass
13 changes: 13 additions & 0 deletions tests/cpython/test_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from cpython import PyTest, CTest, RustTest


class _TestDefault:
def test_default(self):
self.assertEqual(
self.dumps(type, default=repr),
self.dumps(repr(type)))


# class TestPyDefault(_TestDefault, PyTest): pass
# class TestCDefault(_TestDefault, CTest): pass
class TestRustDefault(_TestDefault, RustTest): pass
82 changes: 82 additions & 0 deletions tests/cpython/test_dump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from io import StringIO
from cpython import PyTest, CTest, RustTest

from test.support import bigmemtest, _1G


def ignore_whitespace(a):
"""
Compare two base strings, disregarding whitespace
Adapted from https://github.com/dsindex/blog/wiki/%5Bpython%5D-string-compare-disregarding-white-space
"""
WHITE_MAP = dict.fromkeys(ord(c) for c in string.whitespace)
return a.translate(WHITE_MAP)


class _TestDump:
def test_dump(self):
sio = StringIO()
self.json.dump({}, sio)
self.assertEqual(sio.getvalue(), '{}')

def test_dumps(self):
self.assertEqual(self.dumps({}), '{}')

def test_encode_truefalse(self):
self.assertEqual(self.dumps(
{True: False, False: True}, sort_keys=True),
'{"false":true,"true":false}')
self.assertEqual(self.dumps(
{2: 3.0, 4.0: 5, False: 1, 6: True}, sort_keys=True),
'{"false": 1, "2": 3.0, "4.0": 5, "6": true}')

# Issue 16228: Crash on encoding resized list
def test_encode_mutated(self):
a = [object()] * 10

def crasher(obj):
del a[-1]
self.assertEqual(self.dumps(a, default=crasher),
'[null, null, null, null, null]')

# Issue 24094
def test_encode_evil_dict(self):
class D(dict):
def keys(self):
return L

class X:
def __hash__(self):
del L[0]
return 1337

def __lt__(self, o):
return 0

L = [X() for i in range(1122)]
d = D()
d[1337] = "true.dat"
self.assertEqual(self.dumps(d, sort_keys=True), '{"1337":"true.dat"}')


# class TestPyDump(_TestDump, PyTest): pass

# class TestCDump(_TestDump, CTest):

# # The size requirement here is hopefully over-estimated (actual
# # memory consumption depending on implementation details, and also
# # system memory management, since this may allocate a lot of
# # small objects).

# @bigmemtest(size=_1G, memuse=1)
# def test_large_list(self, size):
# N = int(30 * 1024 * 1024 * (size / _1G))
# l = [1] * N
# encoded = self.dumps(l)
# self.assertEqual(len(encoded), N * 3)
# self.assertEqual(encoded[:1], "[")
# self.assertEqual(encoded[-2:], "1]")
# self.assertEqual(encoded[1:-2], "1, " * (N - 1))

class TestRustDump(_TestDump, RustTest):
pass
Loading