Verified Commit c3fe6004 authored by Sebastian Endres's avatar Sebastian Endres
Browse files

Add UrlOrPath, ...

parent 41659b44
......@@ -10,7 +10,7 @@ from matplotlib import pyplot as plt
from termcolor import colored, cprint
from result_parser import Result
from utils import Subplot, existing_file_path_or_url
from utils import Subplot
SAME_AVG_THRESH_PERC = 0.05
SAME_VAR_THRESH_PERC = 0.10
......@@ -35,12 +35,12 @@ def parse_args():
)
parser.add_argument(
"result1",
type=existing_file_path_or_url,
type=Result,
help="Result.",
)
parser.add_argument(
"result2",
type=existing_file_path_or_url,
type=Result,
help="Result.",
)
parser.add_argument(
......@@ -48,6 +48,7 @@ def parse_args():
type=str,
help="The measurement abbr to compare.",
)
return parser.parse_args()
......@@ -55,6 +56,7 @@ def fetch_result(url: str) -> Result:
result = requests.get(url)
result.raise_for_status()
data = result.json()
return Result(url, data)
......@@ -64,14 +66,12 @@ class CompareCli:
result1: Result,
result2: Result,
measurement: str,
labels: tuple[str, str],
plot=False,
output=None,
):
self.result1 = result1
self.result2 = result2
self.measurement = measurement
self.labels = labels
self.plot = plot
self.output = output
self._unit = ""
......@@ -114,6 +114,7 @@ class CompareCli:
for meas_result2 in measurements2:
combi: str = meas_result2.combination
meas_result1 = lookup1.pop(combi, None)
if not meas_result1 or (
meas_result1.result == "unsupported"
and meas_result2.result != "unsupported"
......@@ -150,6 +151,7 @@ class CompareCli:
(meas_result1.var, meas_result2.var, var_dev),
high_avg_dev or high_var_dev,
)
if same_avg and same_var:
key = "same avg and var"
num_almost_equal += 1
......@@ -208,6 +210,7 @@ class CompareCli:
def error_helper(prop: str):
lst = self.result_comparison[prop]
cprint(f"{prop} ({len(lst)}):", color="red", attrs=["bold"])
for entry in lst:
cprint(f" - {entry}", color="red")
......@@ -216,6 +219,7 @@ class CompareCli:
def detailed_helper(prop: str, color: str):
lst = self.result_comparison[prop]
cprint(f"{prop} ({len(lst)}):", color=color, attrs=["bold"])
for entry in lst:
cprint(
f" - {entry[0]}\t ({entry[1][0]} / {entry[1][1]} ± {entry[2][0]} / {entry[2][1]} | deviation: {entry[1][2] * 100:.0f} % ± {entry[2][2] * 100:.0f} %)",
......@@ -254,19 +258,49 @@ class CompareCli:
*(x[1][1] for x in self.result_comparison["different avg same var"]),
*(x[1][1] for x in self.result_comparison["different avg and var"]),
]
avg1 = sum(avgs1) / len(avgs1)
avg2 = sum(avgs2) / len(avgs2)
assert len(avgs1) == len(avgs2)
if self.result1.file_path.name != self.result2.file_path.name:
label1 = self.result1.file_path.name
label2 = self.result2.file_path.name
elif self.result1.file_path.is_path != self.result2.file_path.is_path:
label1 = (
"local"
if self.result1.file_path.is_path
else f"online\n{self.result1.file_path.mtime.strftime('%Y-%m-%d %H:%M')}"
)
label2 = (
"local"
if self.result2.file_path.is_path
else f"online\n{self.result1.file_path.mtime.strftime('%Y-%m-%d %H:%M')}"
)
else:
label1 = str(self.result1.file_path)
label2 = str(self.result2.file_path)
with Subplot() as (fig, ax):
ax.set_ylabel("Average Data Rate of Implementation Combination")
ax.set_title(f"Comparison of Results of Measurement {self.measurement}")
ax.set_title(
f"Comparison of Results of Measurement {self.measurement}"
f"\n({len(avgs1)} combinations)"
)
ax.yaxis.set_major_formatter(lambda val, _pos: f"{int(val)} {self._unit}")
ax.boxplot([avgs1, avgs2], labels=self.labels)
ax.boxplot(
[avgs1, avgs2],
labels=[
f"{label1}\n(avg. {avg1:.0f} {self._unit})",
f"{label2}\n(avg. {avg2:.0f} {self._unit})",
],
)
if self.output:
fig.savefig(self.output, bbox_inches="tight")
else:
plt.show()
def run(self):
"""docstring for main"""
self.pretty_print_compare_result()
if self.plot:
......@@ -275,32 +309,10 @@ class CompareCli:
def main():
args = parse_args()
result1 = (
Result(args.result1)
if isinstance(args.result1, Path)
else fetch_result(args.result1.geturl())
)
result2 = (
Result(args.result2)
if isinstance(args.result2, Path)
else fetch_result(args.result2.geturl())
)
cli = CompareCli(
result1=result1,
result2=result2,
result1=args.result1,
result2=args.result2,
measurement=args.measurement,
labels=(
(
args.result1.name
if isinstance(args.result1, Path)
else args.result1.geturl()
),
(
args.result2.name
if isinstance(args.result2, Path)
else args.result2.geturl()
),
),
plot=args.plot,
output=args.output,
)
......
......@@ -135,10 +135,12 @@ class PlotAllCli:
for repetition_dir in measurement_result.repetition_log_dirs:
base_sim_path = repetition_dir / "sim"
trace_triple = TraceTriple(
left_pcap_path=base_sim_path
/ f"trace_node_{Side.LEFT.value}_with_secrets.pcapng",
right_pcap_path=base_sim_path
/ f"trace_node_{Side.RIGHT.value}_with_secrets.pcapng",
left_pcap_path=(
base_sim_path / f"trace_node_{Side.LEFT.value}_with_secrets.pcapng"
).resolve(),
right_pcap_path=(
base_sim_path / f"trace_node_{Side.RIGHT.value}_with_secrets.pcapng"
).resolve(),
)
if not trace_triple.left_pcap_path.is_file():
......
......@@ -376,6 +376,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "nest-asyncio"
version = "1.5.1"
description = "Patch asyncio to allow nested event loops"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "nodeenv"
version = "1.6.0"
......@@ -721,7 +729,7 @@ termcolor = ">=1.1.0,<2.0.0"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.11"
content-hash = "007b6a19c94611d8194b0adbe382ad4a0496d6a76a9571ff4e57e1d6de818c83"
content-hash = "5861876e591f6b97db811198cc6f1924d8845b81c574d792732c7491c0dd03f8"
[metadata.files]
appdirs = [
......@@ -1022,6 +1030,10 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nest-asyncio = [
{file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"},
{file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"},
]
nodeenv = [
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
......
......@@ -18,6 +18,8 @@ humanize = "^3.11.0"
Jinja2 = "^3.0.1"
prettytable = "^2.2.0"
requests = "^2.26.0"
urllib3 = "^1.26.7"
nest-asyncio = "^1.5.1"
[tool.poetry.dev-dependencies]
pylint = "^2.6.0"
......
......@@ -12,6 +12,8 @@ from itertools import chain
from pathlib import Path
from typing import Literal, Optional, TypedDict, Union, cast
from utils import UrlOrPath
DETAILS_RE = re.compile(r"(?P<avg>\d+) \(± (?P<var>\d+)\) (?P<unit>\w+)")
......@@ -125,7 +127,7 @@ class _ExtendedTestResultMixin(ABC):
server: Implementation
client: Implementation
test: TestDescription
_base_log_dir: Path
_base_log_dir: UrlOrPath
@property
def succeeded(self) -> bool:
......@@ -140,7 +142,7 @@ class _ExtendedTestResultMixin(ABC):
return f"{self.server.name}_{self.client.name}"
@property
def log_dir_for_test(self) -> Path:
def log_dir_for_test(self) -> UrlOrPath:
"""Return the log dir for this test."""
return self._base_log_dir / self.combination / self.test.name
......@@ -172,10 +174,11 @@ class ExtendedMeasurementResult(_ExtendedTestResultMixin):
@cached_property
def repetition_log_dirs(self) -> list[Path]:
"""Return a list of log dirs for each test repetition."""
assert self.log_dir_for_test.is_path
try:
repetitions = sorted(
iterdir
for iterdir in self.log_dir_for_test.iterdir()
for iterdir in self.log_dir_for_test.path.iterdir()
if iterdir.is_dir() and iterdir.name.isnumeric()
)
repetition_nums = [int(iterdir.name) for iterdir in repetitions]
......@@ -250,7 +253,7 @@ class Result:
def __init__(
self, file_path: Union[Path, str], raw_data: Optional[RawResult] = None
):
self.file_path = Path(file_path)
self.file_path = UrlOrPath(file_path)
self._raw_data = raw_data
self._test_results: Optional[TestResults] = None
self._measurement_results: Optional[MeasurementResults] = None
......@@ -258,19 +261,22 @@ class Result:
def __str__(self):
return (
f"<Result {self.file_path.name} "
f"<{self.__class__.__name__} {self.file_path.path.name} "
f"{len(self.tests)} test(s) "
f"{len(self.implementations)} impl(s)>"
)
def __repr__(self):
return f"{self.__class__.__name__}({repr(self.file_path)})"
@property
def raw_data(self) -> RawResult:
"""Load and return the raw json data."""
if not self._raw_data:
with self.file_path.open("r") as file:
self._raw_data = cast(RawResult, json.load(file))
assert self._raw_data
content = self.file_path.read()
self._raw_data = json.loads(content)
assert self._raw_data
return self._raw_data
......@@ -325,12 +331,12 @@ class Result:
self.raw_data["quic_version"] = hex(value)
@property
def log_dir(self) -> Path:
def log_dir(self) -> UrlOrPath:
"""The path to the detailed logs."""
log_dir = Path(self.raw_data["log_dir"])
log_dir = UrlOrPath(self.raw_data["log_dir"])
if not log_dir.is_absolute():
abs_log_dir = self.file_path.parent / log_dir
abs_log_dir = UrlOrPath(self.file_path.parent / log_dir)
if abs_log_dir.is_dir():
log_dir = abs_log_dir
......@@ -341,7 +347,7 @@ class Result:
# self.file_path,
# log_dir,
# )
else:
elif self.file_path.is_path:
logging.warning(
"The log dir %s given in %s does not exist", log_dir, self.file_path
)
......@@ -349,7 +355,7 @@ class Result:
return log_dir
@log_dir.setter
def log_dir(self, value: Path):
def log_dir(self, value: UrlOrPath):
self.raw_data["log_dir"] = str(value)
def _update_implementations(
......@@ -449,6 +455,16 @@ class Result:
return tests
@property
def measurement_descriptions(self) -> dict[str, MeasurmentDescription]:
"""Return a dict of measurment abbrs and their descriptions."""
return {
abbr: meas
for abbr, meas in self.tests.items()
if isinstance(meas, MeasurmentDescription)
}
@property
def test_results(self) -> TestResults:
"""
......@@ -811,5 +827,6 @@ class Result:
def save(self):
"""Save to file."""
with self.file_path.open("w") as file:
assert self.file_path.is_path
with self.file_path.path.open("w") as file:
json.dump(self.raw_data, fp=file)
......@@ -4,6 +4,7 @@
# pylint: disable=protected-access
import argparse
import json
import pickle
import subprocess
import sys
......@@ -96,6 +97,8 @@ class Trace:
self._keylog_file = keylog_file
override_prefs = {}
file = file.resolve()
if keylog_file is not None:
override_prefs["ssl.keylog_file"] = keylog_file
......@@ -675,6 +678,11 @@ class Trace:
facts["plt"] = pglt
facts["rtt"] = rtt
# write facts file:
facts_file = self.input_file.parent / f"{self.input_file.stem}.facts.json"
with facts_file.open("w") as file:
json.dump(facts, fp=file)
return facts
def _get_rtt(self) -> float:
......
......@@ -7,15 +7,17 @@ import statistics
import sys
import typing
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Callable, NamedTuple, Optional, TypeVar, Union
from urllib.parse import ParseResult as URL
from urllib.parse import urlparse
import humanize
import requests
import termcolor
from dateutil.parser import parse as parse_date
from matplotlib import pyplot as plt
from termcolor import colored, cprint
from urllib3.util.url import Url, parse_url
from yaspin import yaspin
if typing.TYPE_CHECKING:
......@@ -250,24 +252,131 @@ def existing_file_path(value: str, allow_none=False) -> Optional[Path]:
return path
def existing_file_path_or_url(value: str) -> Union[Path, URL]:
"""
An existing file or an URL for argparse.
"""
try:
path = existing_file_path(value)
assert path
class UrlOrPath:
def __init__(self, src: Union[str, Path, Url, "UrlOrPath"]):
if isinstance(src, UrlOrPath):
self.src: Union[Url, Path] = src.src
elif isinstance(src, Url):
self.src = src
elif isinstance(src, Path):
self.src = src
else:
url = parse_url(src)
return path
except argparse.ArgumentTypeError as err:
url = urlparse(value)
if not url.host or not url.scheme:
self.src = Path(src)
else:
self.src = url
if not url.scheme or not url.netloc:
raise argparse.ArgumentTypeError(
"Argument seems neither to be a existing file nor an URL."
) from err
def __str__(self):
return str(self.src)
def __repr__(self):
return f"{self.__class__.__name__}({repr(self.src)})"
@property
def is_path(self):
return isinstance(self.src, Path)
def read(self, mode="r"):
if isinstance(self.src, Path):
with self.path.open(mode) as file:
return file.read()
else:
resp = requests.get(self.src)
resp.raise_for_status()
return resp.text
@property
def scheme(self):
return self.src.scheme if isinstance(self.src, Url) else None
@property
def auth(self):
return self.src.auth if isinstance(self.src, Url) else None
@property
def host(self):
return self.src.host if isinstance(self.src, Url) else None
@property
def port(self):
return self.src.port if isinstance(self.src, Url) else None
@property
def path(self) -> Path:
return Path(self.src.path) if isinstance(self.src, Url) else self.src
@property
def url(self) -> Url:
return Url(
scheme=self.scheme,
auth=self.auth,
host=self.host,
port=self.port,
path=str(self.path),
)
@property
def parent(self) -> "UrlOrPath":
return UrlOrPath(
Url(
scheme=self.scheme,
auth=self.auth,
host=self.host,
port=self.port,
path=str(self.path.parent),
)
)
def __truediv__(self, other: Union[str, Path]) -> "UrlOrPath":
return UrlOrPath(
Url(
scheme=self.scheme,
auth=self.auth,
host=self.host,
port=self.port,
path=str(self.path / other),
)
)
def __rtruediv__(self, other: Union[str, Path]) -> "UrlOrPath":
return UrlOrPath(
Url(
scheme=self.scheme,
auth=self.auth,
host=self.host,
port=self.port,
path=str(other / self.path),
)
)
def is_dir(self) -> bool:
return self.path.is_dir()
def is_absolute(self) -> bool:
return self.path.is_absolute()
@property
def name(self):
return self.path.name
@property
def mtime(self) -> datetime:
"""The modification date"""
if isinstance(self.src, Path):
return datetime.fromtimestamp(self.path.stat().st_mtime)
else:
resp = requests.head(self.src)
resp.raise_for_status()
return parse_date(resp.headers["Last-Modified"])
return url
@mtime.setter
def mtime(self, value: datetime):
self._mtime = value
@dataclass
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment