Skip to content

Commit

Permalink
test: add JSON schema fuzz test strategy (#32)
Browse files Browse the repository at this point in the history
* test: add JSON schema fuzz test strategy

* test: add parametrized tests using examples from ethPM spec

* refactor: rename test file to be more accurate

* refactor: remove unnecessary tests

* refactor: leverage dataclasses more

* fix: json serialization must sort keys per EIP; add EIP notes

* fix: `name` and `version` in Manifest can be empty, add `manifest` default

* docs: add EIP notes to manifest

* test: add xfail to schema test because the schema produces erronous data

* refactor: fixup unprocessed dicts
  • Loading branch information
fubuloubu authored Apr 23, 2021
1 parent 344724e commit a5cbce5
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 531 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"pytest-xdist", # multi-process runner
"pytest-cov", # Coverage analyzer plugin
"hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer
"hypothesis-jsonschema==0.19.0", # JSON Schema fuzzer extension
],
"lint": [
"black>=20.8b1,<21.0", # auto-formatter and linter
Expand Down
62 changes: 62 additions & 0 deletions src/ape/types/abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import json
from copy import deepcopy
from pathlib import Path
from typing import Dict

import dataclassy as dc


def update_params(params, param_name, param_type):
if param_name in params and params[param_name]:
params[param_name] = param_type.from_dict(params[param_name])


def update_list_params(params, param_name, param_type):
if param_name in params and params[param_name]:
params[param_name] = [param_type.from_dict(p) for p in params[param_name]]


def update_dict_params(params, param_name, param_type):
if param_name in params and params[param_name]:
for key in params[param_name]:
params[param_name][key] = param_type.from_dict(params[param_name][key])


def remove_none_fields(data):
if isinstance(data, dict):
return {
k: remove_none_fields(v)
for k, v in data.items()
if v is not None and remove_none_fields(v) is not None
}

elif isinstance(data, list):
return [
remove_none_fields(v)
for v in data
if v is not None and remove_none_fields(v) is not None
]

return data


@dc.dataclass(slots=True, kwargs=True)
class SerializableType:
def to_dict(self) -> Dict:
return remove_none_fields({k: v for k, v in dc.asdict(self).items() if v})

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)
return cls(**params) # type: ignore


class FileMixin(SerializableType):
@classmethod
def from_file(cls, path: Path):
return cls.from_dict(json.load(path.open()))

def to_file(self, path: Path):
# NOTE: EIP-2678 specifies document *must* be tightly packed
# NOTE: EIP-2678 specifies document *must* have sorted keys
json.dump(self.to_dict(), path.open("w"), indent=4, sort_keys=True)
205 changes: 40 additions & 165 deletions src/ape/types/contract.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,59 @@
import urllib.request
from copy import deepcopy
from typing import Dict, List, Optional

import dataclassy as dc
from .abstract import SerializableType, update_list_params, update_params


# TODO link references & link values are for solidity, not used with Vyper
# Offsets are for dynamic links, e.g. doggie's proxy forwarder
@dc.dataclass()
class LinkDependency:
class LinkDependency(SerializableType):
offsets: List[int]
type: str
value: str


@dc.dataclass()
class LinkReference:
class LinkReference(SerializableType):
offsets: List[int]
length: int
name: Optional[str]
name: Optional[str] = None

@classmethod
def from_dict(cls, data: Dict) -> "LinkReference":
data = deepcopy(data)
if "name" not in data:
data["name"] = None
return LinkReference(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.name is None:
del data["name"]
return data


@dc.dataclass()
class Bytecode:
bytecode: str
class Bytecode(SerializableType):
bytecode: Optional[str] = None
linkReferences: Optional[List[LinkReference]] = None
linkDependencies: Optional[List[LinkDependency]] = None

@classmethod
def from_dict(cls, data: Dict) -> "Bytecode":
data = deepcopy(data)
if data.get("linkReferences"):
data["linkReferences"] = [LinkReference.from_dict(lr) for lr in data["linkReferences"]]
else:
data["linkReferences"] = None
if data.get("linkDependencies"):
data["linkDependencies"] = [
LinkDependency(**lr) for lr in data["linkDependencies"] # type: ignore
]
else:
data["linkDependencies"] = None
return Bytecode(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.linkReferences is None:
del data["linkReferences"]
else:
data["linkReferences"] = [lr.to_dict() for lr in self.linkReferences]
if self.linkDependencies is None:
del data["linkDependencies"]
return data
def from_dict(cls, params: Dict):
params = deepcopy(params)
update_list_params(params, "linkReferences", LinkReference)
update_list_params(params, "linkDependencies", LinkDependency)
return cls(**params) # type: ignore


@dc.dataclass()
class ContractInstance:
class ContractInstance(SerializableType):
contractType: str
address: str
transaction: Optional[str] = None
block: Optional[str] = None
runtimeBytecode: Optional[Bytecode] = None

@classmethod
def from_dict(cls, data: Dict) -> "ContractInstance":
data = deepcopy(data)
if "transaction" not in data:
data["transaction"] = None
if "block" not in data:
data["block"] = None
if data.get("runtimeBytecode"):
data["runtimeBytecode"] = Bytecode.from_dict(data["runtimeBytecode"])
else:
data["runtimeBytecode"] = None
return ContractInstance(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.transaction is None:
del data["transaction"]
if self.block is None:
del data["block"]
if self.runtimeBytecode is None:
del data["runtimeBytecode"]
else:
data["runtimeBytecode"] = self.runtimeBytecode.to_dict()
return data
def from_dict(cls, params: Dict):
params = deepcopy(params)
update_params(params, "runtimeBytecode", Bytecode)
return cls(**params) # type: ignore


@dc.dataclass()
class Compiler:
class Compiler(SerializableType):
name: str
version: str
settings: Optional[str] = None
contractTypes: Optional[List[str]] = None

@classmethod
def from_dict(cls, data: Dict) -> "Compiler":
data = deepcopy(data)
if "settings" not in data:
data["settings"] = None
if "contractTypes" not in data:
data["contractTypes"] = None
return Compiler(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.settings is None:
del data["settings"]
if self.contractTypes is None:
del data["contractTypes"]
return data


@dc.dataclass()
class ContractType:
class ContractType(SerializableType):
contractName: str
sourceId: Optional[str] = None
deploymentBytecode: Optional[Bytecode] = None
Expand All @@ -135,59 +63,31 @@ class ContractType:
userdoc: Optional[str] = None
devdoc: Optional[str] = None

@classmethod
def from_dict(cls, data: Dict) -> "ContractType":
data = deepcopy(data)
if "sourceId" not in data:
data["sourceId"] = None
if data.get("deploymentBytecode"):
data["deploymentBytecode"] = Bytecode.from_dict(data["deploymentBytecode"])
else:
data["deploymentBytecode"] = None
if data.get("runtimeBytecode"):
data["runtimeBytecode"] = Bytecode.from_dict(data["runtimeBytecode"])
else:
data["runtimeBytecode"] = None
if "abi" not in data:
data["abi"] = None
if "userdoc" not in data:
data["userdoc"] = None
if "devdoc" not in data:
data["devdoc"] = None
return ContractType(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.sourceId is None:
del data["sourceId"]
if self.deploymentBytecode is None:
del data["deploymentBytecode"]
else:
data["deploymentBytecode"] = self.deploymentBytecode.to_dict()
if self.runtimeBytecode is None:
del data["runtimeBytecode"]
else:
data["runtimeBytecode"] = self.runtimeBytecode.to_dict()
if self.abi is None:
del data["abi"]
if self.userdoc is None:
del data["userdoc"]
if self.devdoc is None:
del data["devdoc"]
def to_dict(self):
data = super().to_dict()

if "abi" in data:
data["abi"] = self.abi # NOTE: Don't prune this one of empty lists

return data

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)
update_params(params, "deploymentBytecode", Bytecode)
update_params(params, "runtimeBytecode", Bytecode)
return cls(**params) # type: ignore

@dc.dataclass()
class Checksum:

class Checksum(SerializableType):
algorithm: str
hash: str


@dc.dataclass()
class Source:
checksum: Checksum
class Source(SerializableType):
checksum: Optional[Checksum] = None
urls: List[str]
content: str
content: Optional[str] = None
# TODO This was probably done for solidity, needs files cached to disk for compiling
# If processing a local project, code already exists, so no issue
# If processing remote project, cache them in ape project data folder
Expand All @@ -200,36 +100,11 @@ def load_content(self):
if len(self.urls) == 0:
return

import urllib.request

response = urllib.request.urlopen(self.urls[0])
self.content = response.read().decode("utf-8")

@classmethod
def from_dict(cls, data: Dict) -> "Source":
data = deepcopy(data)
if data.get("checksum"):
data["checksum"] = Checksum(**data["checksum"]) # type: ignore
else:
data["checksum"] = None
if "urls" not in data:
data["urls"] = None
if "content" not in data:
data["content"] = None
if "installPath" not in data:
data["installPath"] = None
if "type" not in data:
data["type"] = None
if "license" not in data:
data["license"] = None
return Source(**data) # type: ignore

def to_dict(self) -> Dict:
data = dc.asdict(self)
if self.installPath is None:
del data["installPath"]
if self.type is None:
del data["type"]
if self.license is None:
del data["license"]
return data
def from_dict(cls, params: Dict):
params = deepcopy(params)
update_params(params, "checksum", Checksum)
return cls(**params) # type: ignore
Loading

0 comments on commit a5cbce5

Please sign in to comment.