From 72c923732c7b52f15b041c67019c7dfee54c4581 Mon Sep 17 00:00:00 2001 From: Jiri Pavela Date: Wed, 7 Aug 2024 16:18:20 +0200 Subject: [PATCH] Add support for import from CSV and import dir Perun import now supports specification of imported profiles through CSV files and allows to specify the import directory. CLI and CSV interface were extended with the ability to specify custom profile stats. --- perun/cli_groups/import_cli.py | 36 +++--- perun/profile/helpers.py | 51 +++++++- perun/profile/imports.py | 199 ++++++++++++++++++++++--------- perun/utils/common/diff_kit.py | 89 +++++++++++++- perun/view_diff/report/run.py | 60 +++++----- tests/sources/imports/import.csv | 3 + tests/test_imports.py | 20 +++- 7 files changed, 354 insertions(+), 104 deletions(-) create mode 100644 tests/sources/imports/import.csv diff --git a/perun/cli_groups/import_cli.py b/perun/cli_groups/import_cli.py index e151b226..bb5aa6bf 100755 --- a/perun/cli_groups/import_cli.py +++ b/perun/cli_groups/import_cli.py @@ -11,7 +11,6 @@ # Perun Imports from perun.logic import commands from perun.profile import imports -from perun.utils.common import cli_kit @click.group("import") @@ -19,8 +18,15 @@ "--machine-info", "-i", type=click.Path(resolve_path=True, readable=True), - help="Imports machine info from file in JSON format (by default, machine info is loaded from the current host)." - "You can use `utils/generate_machine_info.sh` script to generate the machine info file.", + help="Imports machine info from file in JSON format (by default, machine info is loaded from " + "the current host). You can use `utils/generate_machine_info.sh` script to generate the " + "machine info file.", +) +@click.option( + "--import-dir", + "-d", + type=click.Path(resolve_path=True, readable=True), + help="Specifies the directory to import profiles from.", ) @click.option( "--minor-version", @@ -31,18 +37,18 @@ help="Specifies the head minor version, for which the profiles will be imported.", ) @click.option( - "--exitcode", - "-e", + "--stats-info", + "-t", nargs=1, - required=False, - default="?", - help=("Exit code of the command."), + default=None, + metavar="", + help="Describes the stats associated with the imported profiles. Please see the import " + "documentation for details regarding the stat description format.", ) @click.option( "--cmd", "-c", nargs=1, - required=False, default="", help=( "Command that was being profiled. Either corresponds to some" @@ -53,7 +59,6 @@ "--workload", "-w", nargs=1, - required=False, default="", help="Inputs for . E.g. ``./subdir`` is possible workload for ``ls`` command.", ) @@ -85,7 +90,8 @@ def perf_group(ctx: click.Context, **kwargs: Any) -> None: This supports either profiles collected in: 1. Binary format: e.g., `collected.data` files, that are results of `perf record` - 2. Text format: result of `perf script` that parses the binary into user-friendly and parsing-friendly text format + 2. Text format: result of `perf script` that parses the binary into user-friendly and + parsing-friendly text format """ ctx.obj.update(kwargs) @@ -107,7 +113,7 @@ def from_binary(ctx: click.Context, imported: list[str], **kwargs: Any) -> None: @perf_group.command("script") -@click.argument("imported", type=click.Path(resolve_path=True), nargs=-1, required=True) +@click.argument("imported", type=str, nargs=-1, required=True) @click.pass_context def from_text(ctx: click.Context, imported: list[str], **kwargs: Any) -> None: """Import Perun profiles from output generated by `perf script` command""" @@ -116,9 +122,11 @@ def from_text(ctx: click.Context, imported: list[str], **kwargs: Any) -> None: @perf_group.command("stack") -@click.argument("imported", type=click.Path(resolve_path=True), nargs=-1, required=True) +@click.argument("imported", type=str, nargs=-1, required=True) @click.pass_context def from_stacks(ctx: click.Context, imported: list[str], **kwargs: Any) -> None: - """Import Perun profiles from output generated by `perf script | stackcollapse-perf.pl` command""" + """Import Perun profiles from output generated by `perf script | stackcollapse-perf.pl` + command + """ kwargs.update(ctx.obj) imports.import_perf_from_stack(imported, **kwargs) diff --git a/perun/profile/helpers.py b/perun/profile/helpers.py index dec8a385..062d605c 100644 --- a/perun/profile/helpers.py +++ b/perun/profile/helpers.py @@ -17,12 +17,13 @@ from __future__ import annotations # Standard Imports -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, ClassVar import json import operator import os import re import time +from dataclasses import dataclass # Third-Party Imports @@ -609,3 +610,51 @@ def is_compatible_with_profile(self, profile: profiles.Profile) -> bool: "checksum", "source", ] + + +@dataclass +class ProfileStat: + ALLOWED_ORDERING: ClassVar[dict[str, bool]] = { + "higher_is_better": True, + "lower_is_better": False, + } + + name: str + unit: str = "#" + ordering: bool = True + tooltip: str = "" + value: int | float = 0.0 + + @classmethod + def from_string( + cls, + name: str = "empty", + unit: str = "#", + ordering: str = "higher_is_better", + tooltip: str = "", + *_: Any, + ) -> ProfileStat: + if name == "empty": + # Invalid stat specification, warn + perun_log.warn("Empty profile stat specification. Creating a dummy 'empty' stat.") + if ordering not in cls.ALLOWED_ORDERING: + # Invalid stat ordering, warn + perun_log.warn( + f"Unknown stat ordering: {ordering}. Please choose one of " + f"({', '.join(cls.ALLOWED_ORDERING.keys())}). " + f"Using the default stat ordering value." + ) + ordering_bool = ProfileStat.ordering + else: + ordering_bool = cls.ALLOWED_ORDERING[ordering] + return cls(name, unit, ordering_bool, tooltip) + + def get_normalized_tooltip(self) -> str: + # Find the string representation of the ordering to use in the tooltip + ordering: str = "" + for str_desc, bool_repr in self.ALLOWED_ORDERING.items(): + if bool_repr == self.ordering: + ordering = str_desc.replace("_", " ") + if self.tooltip: + return f"{self.tooltip} ({ordering})" + return ordering diff --git a/perun/profile/imports.py b/perun/profile/imports.py index 1402c531..6e8784cd 100755 --- a/perun/profile/imports.py +++ b/perun/profile/imports.py @@ -3,17 +3,21 @@ from __future__ import annotations # Standard Imports -from typing import Any, Optional +from typing import Any, Optional, Iterator, Callable +from pathlib import Path import json +import csv import os import subprocess +import statistics +from dataclasses import dataclass, field, asdict # Third-Party Imports import gzip # Perun Imports from perun.collect.kperf import parser -from perun.profile import helpers as profile_helpers +from perun.profile import helpers as p_helpers from perun.logic import commands, index, pcs from perun.utils import log, streams from perun.utils.common import script_kit @@ -23,22 +27,116 @@ from perun.vcs import vcs_kit -def load_file(filename: str) -> str: - if filename.endswith(".gz"): - with open(filename, "rb") as f: +# TODO: add documentation +# TODO: fix stats in other types of diffviews +# TODO: refactor the perf import type commands: there is a lot of code duplication + + +@dataclass +class ImportProfileSpec: + path: Path + exit_code: int = 0 + values: list[float] = field(default_factory=list) + + +class ImportedProfiles: + __slots__ = "import_dir", "stats", "profiles" + + def __init__(self, targets: list[str], import_dir: str | None, stats_info: str | None) -> None: + self.import_dir: Path = Path(import_dir) if import_dir is not None else Path.cwd() + # Parse the CLI stats if available + self.stats: list[p_helpers.ProfileStat] = [] + self.profiles: list[ImportProfileSpec] = [] + + if stats_info is not None: + self.stats = [ + p_helpers.ProfileStat.from_string(*stat.split("|")) + for stat in stats_info.split(",") + ] + + for target in targets: + if target.lower().endswith(".csv"): + # The input is a csv file + self._parse_import_csv(target) + else: + # The input is a file path + self._add_imported_profile(target.split(",")) + + def __iter__(self) -> Iterator[ImportProfileSpec]: + return iter(self.profiles) + + def __len__(self) -> int: + return len(self.profiles) + + def get_exit_codes(self) -> str: + return ", ".join(str(p.exit_code) for p in self.profiles) + + def aggregate_stats( + self, agg: Callable[[list[float | int]], float] + ) -> Iterator[p_helpers.ProfileStat]: + stat_value_lists: list[list[float | int]] = [[] for _ in range(len(self.stats))] + for profile in self.profiles: + value_list: list[float | int] + stat_value: float | int + for value_list, stat_value in zip(stat_value_lists, profile.values): + value_list.append(stat_value) + for value_list, stat_obj in zip(stat_value_lists, self.stats): + stat_obj.value = agg(value_list) + yield stat_obj + + def _parse_import_csv(self, target: str) -> None: + with open(self.import_dir / target, "r") as csvfile: + csv_reader = csv.reader(csvfile, delimiter=",") + header: list[str] = next(csv_reader) + stats: list[p_helpers.ProfileStat] = [ + p_helpers.ProfileStat.from_string(*stat_definition.split("|")) + for stat_definition in header[2:] + ] + # Parse the CSV stat definition and check that they are not in conflict with the CLI + # stat definitions, if any + for idx, stat in enumerate(stats): + if idx >= len(self.stats): + self.stats.append(stat) + elif stat != self.stats[idx]: + log.warn( + f"Mismatching profile stat definition from CLI and CSV: " + f"cli.{self.stats[idx].name} != csv.{stat.name}. " + f"Using the CLI stat definition." + ) + # Parse the remaining rows that should represent profile specifications + for row in csv_reader: + self._add_imported_profile(row) + + def _add_imported_profile(self, target: list[str]) -> None: + if len(target) == 0: + # Empty profile specification, warn + log.warn("Empty import profile specification. Skipping.") + else: + self.profiles.append( + ImportProfileSpec( + self.import_dir / target[0], + int(target[1]) if len(target) >= 2 else ImportProfileSpec.exit_code, + list(map(float, target[2:])), + ) + ) + + +def load_file(filepath: Path) -> str: + if filepath.suffix.lower() == ".gz": + with open(filepath, "rb") as f: header = f.read(2) f.seek(0) assert header == b"\x1f\x8b" with gzip.GzipFile(fileobj=f) as gz: return gz.read().decode("utf-8") - with open(filename, "r", encoding="utf-8") as imported_handle: + with open(filepath, "r", encoding="utf-8") as imported_handle: return imported_handle.read() def get_machine_info(machine_info: Optional[str] = None) -> dict[str, Any]: """Returns machine info either from input file or constructs it from environment - :param machine info: file in json format, which contains machine specification + :param machine_info: file in json format, which contains machine specification :return: parsed dictionary format of machine specification """ if machine_info is not None: @@ -48,7 +146,8 @@ def get_machine_info(machine_info: Optional[str] = None) -> dict[str, Any]: return environment.get_machine_specification() -def import_from_string( +def import_profile( + profiles: ImportedProfiles, resources: list[dict[str, Any]], minor_version: MinorVersion, machine_info: Optional[str] = None, @@ -66,12 +165,13 @@ def import_from_string( ) prof.update({"origin": minor_version.checksum}) prof.update({"machine": get_machine_info(machine_info)}) + prof.update({"stats": [asdict(stat) for stat in profiles.aggregate_stats(statistics.median)]}), prof.update( { "header": { "type": "time", "cmd": kwargs.get("cmd", ""), - "exitcode": kwargs.get("exitcode", "?"), + "exitcode": profiles.get_exit_codes(), "workload": kwargs.get("workload", ""), "units": {"time": "sample"}, } @@ -84,14 +184,14 @@ def import_from_string( "params": { "with_sudo": with_sudo, "warmup": kwargs.get("warmup", 0), - "repeat": kwargs.get("repeat", 1), + "repeat": len(profiles), }, } } ) prof.update({"postprocessors": []}) - full_profile_name = profile_helpers.generate_profile_name(prof) + full_profile_name = p_helpers.generate_profile_name(prof) profile_directory = pcs.get_job_directory() full_profile_path = os.path.join(profile_directory, full_profile_name) @@ -110,93 +210,78 @@ def import_from_string( @vcs_kit.lookup_minor_version def import_perf_from_record( imported: list[str], - machine_info: Optional[str], + import_dir: str | None, + stats_info: str | None, minor_version: str, with_sudo: bool = False, - save_to_index: bool = False, **kwargs: Any, ) -> None: """Imports profile collected by `perf record`""" + parse_script = script_kit.get_script("stackcollapse-perf.pl") minor_version_info = pcs.vcs().get_minor_version_info(minor_version) - kwargs["repeat"] = len(imported) - parse_script = script_kit.get_script("stackcollapse-perf.pl") - out = b"" + profiles = ImportedProfiles(imported, import_dir, stats_info) resources = [] - for imported_file in imported: + for imported_file in profiles: perf_script_command = ( - f"{'sudo ' if with_sudo else ''}perf script -i {imported_file} | {parse_script}" + f"{'sudo ' if with_sudo else ''}perf script -i {imported_file.path} | {parse_script}" ) try: out, _ = external_commands.run_safely_external_command(perf_script_command) - log.minor_success(f"Raw data from {log.path_style(imported_file)}", "collected") + log.minor_success( + f"Raw data from {log.path_style(str(imported_file.path))}", "collected" + ) except subprocess.CalledProcessError as err: - log.minor_fail(f"Raw data from {log.path_style(imported_file)}", "not collected") + log.minor_fail( + f"Raw data from {log.path_style(str(imported_file.path))}", "not collected" + ) log.error(f"Cannot load data due to: {err}") resources.extend(parser.parse_events(out.decode("utf-8").split("\n"))) - log.minor_success(log.path_style(imported_file), "imported") - import_from_string( - resources, - minor_version_info, - machine_info, - with_sudo=with_sudo, - save_to_index=save_to_index, - **kwargs, - ) + log.minor_success(log.path_style(str(imported_file.path)), "imported") + import_profile(profiles, resources, minor_version_info, with_sudo=with_sudo, **kwargs) @vcs_kit.lookup_minor_version def import_perf_from_script( imported: list[str], - machine_info: Optional[str], + import_dir: str | None, + stats_info: str | None, minor_version: str, - save_to_index: bool = False, **kwargs: Any, ) -> None: """Imports profile collected by `perf record; perf script`""" parse_script = script_kit.get_script("stackcollapse-perf.pl") - out = b"" minor_version_info = pcs.vcs().get_minor_version_info(minor_version) - kwargs["repeat"] = len(imported) + + profiles = ImportedProfiles(imported, import_dir, stats_info) resources = [] - for imported_file in imported: - perf_script_command = f"cat {imported_file} | {parse_script}" + for imported_file in profiles: + perf_script_command = f"cat {imported_file.path} | {parse_script}" out, _ = external_commands.run_safely_external_command(perf_script_command) - log.minor_success(f"Raw data from {log.path_style(imported_file)}", "collected") + log.minor_success(f"Raw data from {log.path_style(str(imported_file.path))}", "collected") resources.extend(parser.parse_events(out.decode("utf-8").split("\n"))) - log.minor_success(log.path_style(imported_file), "imported") - import_from_string( - resources, - minor_version_info, - machine_info, - save_to_index=save_to_index, - **kwargs, - ) + log.minor_success(log.path_style(str(imported_file.path)), "imported") + import_profile(profiles, resources, minor_version_info, **kwargs) @vcs_kit.lookup_minor_version def import_perf_from_stack( imported: list[str], - machine_info: Optional[str], + import_dir: str | None, + stats_info: str | None, minor_version: str, - save_to_index: bool = False, **kwargs: Any, ) -> None: """Imports profile collected by `perf record; perf script | stackcollapse-perf.pl`""" minor_version_info = pcs.vcs().get_minor_version_info(minor_version) - kwargs["repeat"] = len(imported) + profiles = ImportedProfiles(imported, import_dir, stats_info) resources = [] - for imported_file in imported: - out = load_file(imported_file) + + for imported_profile in profiles: + out = load_file(imported_profile.path) resources.extend(parser.parse_events(out.split("\n"))) - log.minor_success(log.path_style(imported_file), "imported") - import_from_string( - resources, - minor_version_info, - machine_info, - save_to_index=save_to_index, - **kwargs, - ) + log.minor_success(log.path_style(str(imported_profile.path)), "imported") + import_profile(profiles, resources, minor_version_info, **kwargs) diff --git a/perun/utils/common/diff_kit.py b/perun/utils/common/diff_kit.py index 0d1f5733..06f466d8 100755 --- a/perun/utils/common/diff_kit.py +++ b/perun/utils/common/diff_kit.py @@ -23,7 +23,8 @@ def save_diff_view( lhs_profile: Profile, rhs_profile: Profile, ) -> str: - """Saves the content to the output file; if no output file is stated, then it is automatically generated + """Saves the content to the output file; if no output file is stated, then it is automatically + generated. :param output_file: file, where the content will be stored :param content: content of the output file @@ -133,11 +134,92 @@ def diff_to_html(diff: list[str], start_tag: Literal["+", "-"]) -> str: result.append(chunk[2:]) if chunk.startswith(start_tag): result.append( - f'{chunk[2:]}' + f'{chunk[2:]}' ) return " ".join(result) +def _color_stat_value_diff( + lhs_stat: helpers.ProfileStat, rhs_stat: helpers.ProfileStat +) -> tuple[str, str]: + """Color the stat values on the LHS and RHS according to their difference. + + The color is determined by the stat ordering and the result of the stat values comparison. + + :param lhs_stat: a stat from the baseline + :param rhs_stat: a stat from the target + :return: colored LHS and RHS stat values + """ + # Map the colors based on the value ordering + color_map: dict[bool, str] = { + lhs_stat.ordering: "red", + not lhs_stat.ordering: "green", + } + lhs_value, rhs_value = str(lhs_stat.value), str(rhs_stat.value) + if lhs_stat.ordering != rhs_stat.ordering: + # Conflicting ordering in baseline and target, do not compare + log.warn( + f"Profile stats '{lhs_stat.name}' have conflicting ordering in baseline and target." + f" The stats will not be compared." + ) + elif lhs_value != rhs_value: + # Different stat values, color them + is_lhs_lower = lhs_stat.value < rhs_stat.value + lhs_value = ( + f'{lhs_value}' + ) + rhs_value = ( + f'{rhs_value}' + ) + return lhs_value, rhs_value + + +def generate_diff_of_stats( + lhs_stats: list[helpers.ProfileStat], rhs_stats: list[helpers.ProfileStat] +) -> tuple[list[tuple[str, str, str]], list[tuple[str, str, str]]]: + """Re-generate the stats with CSS diff styles suitable for an output. + + :param lhs_stats: stats from the baseline + :param rhs_stats: stats from the target + :return: collection of LHS and RHS stats (stat-name [stat-unit], stat-value, stat-tooltip) + with CSS styles that reflect the stat diffs. + """ + + # Get all the stats that occur in either lhs or rhs and match those that exist in both + stats_map: dict[str, dict[str, helpers.ProfileStat]] = {} + for stat_source, stat_list in [("lhs", lhs_stats), ("rhs", rhs_stats)]: + for stat in stat_list: + stats_map.setdefault(stat.name, {})[stat_source] = stat + # Iterate the stats and format them according to their diffs + lhs_diff, rhs_diff = [], [] + for stat_key in sorted(stats_map.keys()): + lhs_stat: helpers.ProfileStat | None = stats_map[stat_key].get("lhs", None) + rhs_stat: helpers.ProfileStat | None = stats_map[stat_key].get("rhs", None) + lhs_tooltip = lhs_stat.get_normalized_tooltip() if lhs_stat is not None else "" + rhs_tooltip = rhs_stat.get_normalized_tooltip() if rhs_stat is not None else "" + if rhs_stat and lhs_stat is None: + # There is no matching stat on the LHS + lhs_diff.append((f"{rhs_stat.name} [{rhs_stat.unit}]", "-", rhs_tooltip)) + rhs_diff.append( + (f"{rhs_stat.name} [{rhs_stat.unit}]", str(rhs_stat.value), rhs_tooltip) + ) + elif lhs_stat and rhs_stat is None: + # There is no matching stat on the RHS + lhs_diff.append( + (f"{lhs_stat.name} [{lhs_stat.unit}]", str(lhs_stat.value), lhs_tooltip) + ) + rhs_diff.append((f"{lhs_stat.name} [{lhs_stat.unit}]", "-", lhs_tooltip)) + elif lhs_stat and rhs_stat: + # The stat is present on both LHS and RHS + lhs_value, rhs_value = _color_stat_value_diff(lhs_stat, rhs_stat) + lhs_diff.append((f"{lhs_stat.name} [{lhs_stat.unit}]", lhs_value, lhs_tooltip)) + rhs_diff.append((f"{rhs_stat.name} [{rhs_stat.unit}]", rhs_value, rhs_tooltip)) + return lhs_diff, rhs_diff + + def generate_diff_of_headers( lhs_header: list[tuple[str, Any, str]], rhs_header: list[tuple[str, Any, str]] ) -> tuple[list[tuple[str, Any, str]], list[tuple[str, Any, str]]]: @@ -150,7 +232,8 @@ def generate_diff_of_headers( for (lhs_key, lhs_value, lhs_info), (rhs_key, rhs_value, _) in zip(lhs_header, rhs_header): assert ( lhs_key == rhs_key - and f"Configuration keys in headers are wrongly order (expected {lhs_key}; got {rhs_key})" + and f"Configuration keys in headers are wrongly ordered (expected {lhs_key}; " + f"got {rhs_key})" ) if lhs_value != rhs_value: diff = list(difflib.ndiff(str(lhs_value).split(), str(rhs_value).split())) diff --git a/perun/view_diff/report/run.py b/perun/view_diff/report/run.py index c1c9fc63..28c8101b 100755 --- a/perun/view_diff/report/run.py +++ b/perun/view_diff/report/run.py @@ -27,7 +27,7 @@ # Perun Imports from perun.logic import config -from perun.profile import convert +from perun.profile import convert, helpers as profile_helpers from perun.profile.factory import Profile from perun.templates import filters, factory as templates from perun.utils import log, mapping @@ -63,7 +63,7 @@ class Config: def __init__(self) -> None: """Initializes the config - By default we consider, that the traces are not inclusive + By default, we consider that the traces are not inclusive """ self.trace_is_inclusive: bool = False self.top_n_traces: int = self.DefaultTopN @@ -71,9 +71,9 @@ def __init__(self) -> None: self.max_seen_trace: int = 0 self.max_per_resource: dict[str, float] = defaultdict(float) self.minimize: bool = False - self.profile_stats: dict[str, dict[str, float]] = { - "baseline": defaultdict(float), - "target": defaultdict(float), + self.profile_stats: dict[str, list[profile_helpers.ProfileStat]] = { + "baseline": [], + "target": [], } @@ -526,13 +526,31 @@ def process_traces( saved_maxima = Config().max_per_resource for key in max_samples.keys(): saved_maxima[key] = max(saved_maxima[key], max_samples[key]) - Config().profile_stats[profile_type][ - f"Overall {key};The overall value of the {key} for the root value" - ] = max_samples[key] - Config().profile_stats[profile_type][ - "Maximal Trace Length;Maximal lenght of the trace in the profile" - ] = max_trace + # TODO: This is a bit of a hack since the key already contains the unit + name, unit = key.split("[", maxsplit=1) + name.rstrip() + unit = unit.rsplit("]", maxsplit=1)[0] + Config().profile_stats[profile_type].append( + profile_helpers.ProfileStat( + f"Overall {name}", + unit, + False, + f"The overall value of the {name} for the root value", + max_samples[key], + ) + ) + Config().profile_stats[profile_type].append( + profile_helpers.ProfileStat( + "Maximum Trace Length", + "#", + False, + "Maximum length of the trace in the profile", + max_trace, + ) + ) Config().max_seen_trace = max(max_trace, Config().max_seen_trace) + for stat in profile.get("stats", []): + Config().profile_stats[profile_type].append(profile_helpers.ProfileStat(**stat)) def generate_trace_stats(graph: Graph) -> dict[str, list[TraceStat]]: @@ -683,21 +701,6 @@ def extract_stats_from_trace( return uid_trace_stats -def generate_profile_stats( - profile_type: Literal["baseline", "target"] -) -> list[tuple[str, Any, str]]: - """Generates stats for baseline or target profile - - :param profile_type: type of the profile - :return: list of tuples containing stats as tuples key, value and tooltip - """ - profile_stats = [] - for key, value in Config().profile_stats[profile_type].items(): - stat_key, stat_tooltip = key.split(";") - profile_stats.append((stat_key, value, stat_tooltip)) - return profile_stats - - def generate_report(lhs_profile: Profile, rhs_profile: Profile, **kwargs: Any) -> None: """Generates differences of two profiles as sankey diagram @@ -736,8 +739,9 @@ def generate_report(lhs_profile: Profile, rhs_profile: Profile, **kwargs: Any) - ) log.minor_success("Sankey graphs", "generated") lhs_header, rhs_header = diff_kit.generate_headers(lhs_profile, rhs_profile) - lhs_stats, rhs_stats = diff_kit.generate_diff_of_headers( - generate_profile_stats("baseline"), generate_profile_stats("target") + lhs_stats, rhs_stats = diff_kit.generate_diff_of_stats( + Config().profile_stats["baseline"], + Config().profile_stats["target"], ) env_filters = {"sanitize_variable_name": filters.sanitize_variable_name} diff --git a/tests/sources/imports/import.csv b/tests/sources/imports/import.csv new file mode 100644 index 00000000..800ae203 --- /dev/null +++ b/tests/sources/imports/import.csv @@ -0,0 +1,3 @@ +#Stackfile,Exit_code,bogo-ops-per-second-real-time|bogo-ops-per-second|higher_is_better,stat2|stat2_unit|stat2_direction +import.stack.gz,0,18511.379883,956.36 +import.stack,0,17894.875698,897.01 \ No newline at end of file diff --git a/tests/test_imports.py b/tests/test_imports.py index f671bba3..791edda7 100755 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -72,6 +72,24 @@ def test_imports(pcs_with_svs): assert result.exit_code == 0 assert len(os.listdir(os.path.join(".perun", "jobs"))) == 4 + result = runner.invoke( + cli.cli, + [ + "import", + "-c", + "ls", + "-w", + "..", + "-d", + pool_path, + "perf", + "stack", + "import.csv", + ], + ) + assert result.exit_code == 0 + assert len(os.listdir(os.path.join(".perun", "jobs"))) == 5 + result = runner.invoke( cli.cli, [ @@ -86,4 +104,4 @@ def test_imports(pcs_with_svs): ], ) assert result.exit_code == 1 - assert len(os.listdir(os.path.join(".perun", "jobs"))) == 4 + assert len(os.listdir(os.path.join(".perun", "jobs"))) == 5