diff --git a/tests/utils/test_git_util.py b/tests/utils/test_git_util.py
index a2dc084ec..d39efaa00 100644
--- a/tests/utils/test_git_util.py
+++ b/tests/utils/test_git_util.py
@@ -28,6 +28,7 @@
get_submodule_head,
calc_code_churn_range,
RepositoryAtCommit,
+ calc_surviving_lines,
)
@@ -226,6 +227,24 @@ def test_contains_source_code_with(self) -> None:
)
)
+ def test_calc_surviving_lines(self):
+ lines = calc_surviving_lines(
+ "MutliMethodAuthorCoordination",
+ FullCommitHash("f2f294bdda48526915b5a018e7e91f9f80204269")
+ )
+ self.assertEqual(
+ lines[FullCommitHash("28f1624bda75a0c2da961e2572f9eebc31998346")], 3
+ )
+ self.assertEqual(
+ lines[FullCommitHash("9209cff2d5b6cf9b7b39020b43081bd840347be2")], 4
+ )
+ self.assertEqual(
+ lines[FullCommitHash("ffb0fb502072846e081ac9f63f1eb86667197b95")], 3
+ )
+ self.assertEqual(
+ lines[FullCommitHash("f2f294bdda48526915b5a018e7e91f9f80204269")], 9
+ )
+
class TestChurnConfig(unittest.TestCase):
"""Test if ChurnConfig sets languages correctly."""
diff --git a/uicomponents/CaseStudyGeneration.ui b/uicomponents/CaseStudyGeneration.ui
index 92b554b2c..6c97fa963 100644
--- a/uicomponents/CaseStudyGeneration.ui
+++ b/uicomponents/CaseStudyGeneration.ui
@@ -10,7 +10,7 @@
0
0
760
- 443
+ 491
@@ -150,6 +150,19 @@
+ -
+
+
+ -
+
+
+ -
+
+
+ Filter CaseStudy´
+
+
+
-
@@ -358,8 +371,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
-<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>
+</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;">
+<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;"><br /></p></body></html>
@@ -374,7 +387,7 @@ p, li { white-space: pre-wrap; }
0
0
760
- 22
+ 34
diff --git a/varats-core/varats/experiment/experiment_util.py b/varats-core/varats/experiment/experiment_util.py
index d70af5353..e0fe45b94 100644
--- a/varats-core/varats/experiment/experiment_util.py
+++ b/varats-core/varats/experiment/experiment_util.py
@@ -366,7 +366,7 @@ def report_spec(cls) -> ReportSpecification:
return cls.REPORT_SPEC
@classmethod
- def file_belongs_to_experiment(cls, file_name: str) -> bool:
+ def file_belongs_to_experiment(cls, file_name: ReportFilename) -> bool:
"""
Checks if the file belongs to this experiment.
@@ -377,7 +377,7 @@ def file_belongs_to_experiment(cls, file_name: str) -> bool:
True, if the file belongs to this experiment type
"""
try:
- other_short_hand = ReportFilename(file_name).experiment_shorthand
+ other_short_hand = file_name.experiment_shorthand
return cls.shorthand() == other_short_hand
except ValueError:
return False
diff --git a/varats-core/varats/mapping/author_map.py b/varats-core/varats/mapping/author_map.py
index b64af1b76..a8e820564 100644
--- a/varats-core/varats/mapping/author_map.py
+++ b/varats-core/varats/mapping/author_map.py
@@ -31,7 +31,7 @@ def __eq__(self, other) -> bool:
return False
def __str__(self) -> str:
- return f"{self.name} <{self.mail}>"
+ return f"{self.name} {self.mail}"
def __repr__(self) -> str:
return f"{self.name} <{self.mail}>; {self.names},{self.mail_addresses}"
diff --git a/varats-core/varats/utils/git_util.py b/varats-core/varats/utils/git_util.py
index 18cb6f714..373319e3c 100644
--- a/varats-core/varats/utils/git_util.py
+++ b/varats-core/varats/utils/git_util.py
@@ -476,7 +476,7 @@ class Language(Enum):
value: tp.Set[str] # pylint: disable=invalid-name
C = {"h", "c"}
- CPP = {"h", "hxx", "hpp", "cxx", "cpp", "cc"}
+ CPP = {"h", "hxx", "hpp", "cxx", "cpp", "cc", "hh"}
def __init__(self) -> None:
self.__enabled_languages: tp.List[ChurnConfig.Language] = []
@@ -1135,7 +1135,7 @@ def branch_has_upstream(
return tp.cast(bool, exit_code == 0)
-class RepositoryAtCommit():
+class RepositoryAtCommit:
"""Context manager to work with a repository at a specific revision, without
duplicating the repository."""
@@ -1143,7 +1143,9 @@ def __init__(self, project_name: str, revision: ShortCommitHash) -> None:
self.__repo = pygit2.Repository(
get_local_project_git_path(project_name)
)
- self.__initial_head = self.__repo.head
+
+ self.__initial_head: pygit2.Reference = self.__repo.head
+ print(self.__initial_head.name)
self.__revision = self.__repo.get(revision.hash)
def __enter__(self) -> Path:
@@ -1155,4 +1157,59 @@ def __exit__(
exc_value: tp.Optional[BaseException],
exc_traceback: tp.Optional[TracebackType]
) -> None:
- self.__repo.checkout(self.__initial_head)
+ self.__repo.checkout(
+ self.__initial_head, strategy=pygit2.GIT_CHECKOUT_FORCE
+ )
+
+
+def calc_surviving_lines(project_name: str, revision: tp.Optional[FullCommitHash] = None) -> \
+tp.Dict[FullCommitHash, int]:
+ """
+ Get the surviving lines of older commits at a given revision.
+
+ Args:
+ project_name: project to analyze
+ revision: revision to analyze at
+
+ returns: number of lines per prior commit
+ """
+ churn_config = ChurnConfig.create_c_style_languages_config()
+ file_pattern = re.compile(
+ "|".join(churn_config.get_extensions_repr(r"^.*\.", r"$"))
+ )
+ if revision is not None:
+ hash = revision.hash
+ else:
+ hash = "HEAD"
+ lines_per_revision: tp.Dict[FullCommitHash, int] = {}
+ repo = pygit2.Repository(get_local_project_git_path(project_name))
+
+ initial_head: pygit2.Reference = repo.head
+ repo_folder = get_local_project_git_path(project_name)
+ git(__get_git_path_arg(repo_folder), "checkout", "-f", hash)
+ files = git(
+ __get_git_path_arg(repo_folder), "ls-tree", "-r", "--name-only", hash
+ ).splitlines()
+
+ for file in files:
+ if file_pattern.match(file):
+ lines = git(
+ __get_git_path_arg(repo_folder), "blame", "--root", "-l",
+ f"{file}"
+ ).splitlines()
+ for line in lines:
+ if line:
+ last_change = line[:FullCommitHash.hash_length()]
+ try:
+ last_change = FullCommitHash(last_change)
+ except ValueError:
+ continue
+
+ if lines_per_revision.keys().__contains__(last_change):
+ lines_per_revision[
+ last_change] = lines_per_revision[last_change] + 1
+ else:
+ lines_per_revision[last_change] = 1
+
+ git(__get_git_path_arg(repo_folder), "checkout", initial_head.name)
+ return lines_per_revision
diff --git a/varats/varats/data/databases/author_interactions_database.py b/varats/varats/data/databases/author_interactions_database.py
new file mode 100644
index 000000000..cd9fe07e6
--- /dev/null
+++ b/varats/varats/data/databases/author_interactions_database.py
@@ -0,0 +1,156 @@
+import typing as tp
+
+import pandas as pd
+
+from varats.data.cache_helper import build_cached_report_table
+from varats.data.databases.evaluationdatabase import EvaluationDatabase
+from varats.data.reports.blame_report import (
+ gen_base_to_inter_commit_repo_pair_mapping,
+)
+from varats.experiments.vara.blame_report_experiment import (
+ BlameReportExperiment,
+)
+from varats.jupyterhelper.file import load_blame_report
+from varats.mapping.author_map import Author, generate_author_map
+from varats.mapping.commit_map import CommitMap
+from varats.paper.case_study import CaseStudy
+from varats.paper_mgmt.case_study import get_case_study_file_name_filter
+from varats.project.project_util import (
+ get_local_project_git_path,
+ get_primary_project_source,
+)
+from varats.report.report import ReportFilepath
+from varats.revision.revisions import (
+ get_processed_revisions_files,
+ get_failed_revisions_files,
+)
+from varats.utils.git_util import (
+ create_commit_lookup_helper,
+ UNCOMMITTED_COMMIT_HASH,
+ CommitRepoPair,
+)
+
+
+class AuthorInteractionsDatabase(
+ EvaluationDatabase,
+ cache_id="author_contribution_data_base",
+ column_types={
+ "author_name": 'str',
+ "author_mail": 'str',
+ "internal_interactions": 'int32',
+ "external_interactions": 'int32'
+ }
+):
+ """Provides access to internal and external interactions of authors."""
+
+ @classmethod
+ def _load_dataframe(
+ cls, project_name: str, commit_map: CommitMap,
+ case_study: tp.Optional[CaseStudy], **kwargs: tp.Dict[str, tp.Any]
+ ) -> pd.DataFrame:
+
+ def create_dataframe_layout() -> pd.DataFrame:
+ df_layout = pd.DataFrame(columns=cls.COLUMNS)
+ df_layout = df_layout.astype(cls.COLUMN_TYPES)
+ return df_layout
+
+ def create_data_frame_for_report(
+ report_path: ReportFilepath
+ ) -> tp.Tuple[pd.DataFrame, str, str]:
+ report = load_blame_report(report_path)
+ base_inter_c_repo_pair_mapping = \
+ gen_base_to_inter_commit_repo_pair_mapping(report)
+ revision = report.head_commit
+
+ def build_dataframe_row(
+ author: Author, internal_interactions: int,
+ external_interactions: int
+ ) -> tp.Dict[str, tp.Any]:
+ data_dict: tp.Dict[str, tp.Any] = {
+ 'revision': revision.hash,
+ 'time_id': commit_map.short_time_id(revision),
+ 'author_name': author.name,
+ 'author_mail': author.mail,
+ 'internal_interactions': internal_interactions,
+ 'external_interactions': external_interactions
+ }
+ return data_dict
+
+ result_data_dicts: tp.Dict[Author, tp.Dict[str, tp.Any]] = {}
+ amap = generate_author_map(project_name)
+ repo_name = get_primary_project_source(project_name).local
+ commit_lookup_helper = create_commit_lookup_helper(project_name)
+ for base_pair in base_inter_c_repo_pair_mapping:
+ if not base_pair.commit.repository_name.startswith(repo_name):
+ # Skip interactions with submodules
+ continue
+ inter_pair_dict = base_inter_c_repo_pair_mapping[base_pair]
+ if base_pair.commit.commit_hash == UNCOMMITTED_COMMIT_HASH:
+ continue
+ base_commit = commit_lookup_helper(
+ CommitRepoPair(base_pair.commit.commit_hash, repo_name)
+ )
+ base_author = amap.get_author(
+ base_commit.author.name, base_commit.author.email
+ )
+ if base_author is None:
+ amap.add_entry(
+ base_commit.author.name, base_commit.author.email
+ )
+ base_author = amap.get_author(
+ base_commit.author.name, base_commit.author.email
+ )
+ internal_interactions = 0
+ external_interactions = 0
+ for inter_pair, interactions in inter_pair_dict.items():
+ if inter_pair.commit.commit_hash == UNCOMMITTED_COMMIT_HASH or not inter_pair.commit.repository_name.startswith(
+ repo_name
+ ):
+ continue
+ inter_commit = commit_lookup_helper(
+ CommitRepoPair(
+ inter_pair.commit.commit_hash, repo_name
+ )
+ )
+ inter_author = amap.get_author(
+ inter_commit.author.name, inter_commit.author.email
+ )
+ if base_author == inter_author:
+ internal_interactions += interactions
+ else:
+ external_interactions += interactions
+ if base_author in result_data_dicts:
+ result_data_dicts[base_author]['internal_interactions'
+ ] += internal_interactions
+ result_data_dicts[base_author]['external_interactions'
+ ] += external_interactions
+ else:
+ result_data_dicts[base_author] = build_dataframe_row(
+ base_author, internal_interactions,
+ external_interactions
+ )
+
+ return pd.DataFrame(
+ list(result_data_dicts.values())
+ ), report.head_commit.hash, str(report_path.stat().st_mtime_ns)
+
+ report_files = get_processed_revisions_files(
+ project_name,
+ BlameReportExperiment,
+ file_name_filter=get_case_study_file_name_filter(case_study)
+ )
+
+ failed_report_files = get_failed_revisions_files(
+ project_name,
+ BlameReportExperiment,
+ file_name_filter=get_case_study_file_name_filter(case_study)
+ )
+
+ data_frame = build_cached_report_table(
+ cls.CACHE_ID, project_name, report_files, failed_report_files,
+ create_dataframe_layout, create_data_frame_for_report,
+ lambda path: path.report_filename.commit_hash.hash,
+ lambda path: str(path.stat().st_mtime_ns),
+ lambda a, b: int(a) > int(b)
+ )
+ return data_frame
diff --git a/varats/varats/data/databases/commit_interaction_aggregate_database.py b/varats/varats/data/databases/commit_interaction_aggregate_database.py
new file mode 100644
index 000000000..6104a4aa8
--- /dev/null
+++ b/varats/varats/data/databases/commit_interaction_aggregate_database.py
@@ -0,0 +1,101 @@
+import typing as tp
+
+import pandas as pd
+
+from varats.data.cache_helper import build_cached_report_table
+from varats.data.databases.evaluationdatabase import EvaluationDatabase
+from varats.data.reports.blame_report import (
+ gen_base_to_inter_commit_repo_pair_mapping,
+)
+from varats.experiments.vara.blame_report_experiment import (
+ BlameReportExperiment,
+)
+from varats.jupyterhelper.file import load_blame_report
+from varats.mapping.commit_map import CommitMap
+from varats.paper.case_study import CaseStudy
+from varats.paper_mgmt.case_study import get_case_study_file_name_filter
+from varats.report.report import ReportFilepath
+from varats.revision.revisions import (
+ get_processed_revisions_files,
+ get_failed_revisions_files,
+)
+from varats.utils.git_util import FullCommitHash
+
+
+class SurvivingInteractionsDatabase(
+ EvaluationDatabase,
+ cache_id="survivng_interactions_data",
+ column_types={
+ "base_hash": 'str',
+ "interactions": 'int32',
+ }
+):
+ """Provides access to total interactions of commits."""
+
+ @classmethod
+ def _load_dataframe(
+ cls, project_name: str, commit_map: CommitMap,
+ case_study: tp.Optional[CaseStudy], **kwargs: tp.Dict[str, tp.Any]
+ ) -> pd.DataFrame:
+
+ def create_dataframe_layout() -> pd.DataFrame:
+ df_layout = pd.DataFrame(columns=cls.COLUMNS)
+ df_layout = df_layout.astype(cls.COLUMN_TYPES)
+ return df_layout
+
+ def create_data_frame_for_report(
+ report_path: ReportFilepath
+ ) -> tp.Tuple[pd.DataFrame, str, str]:
+ report = load_blame_report(report_path)
+ base_inter_c_repo_pair_mapping = \
+ gen_base_to_inter_commit_repo_pair_mapping(report)
+ revision = report.head_commit
+
+ def build_dataframe_row(chash: FullCommitHash,
+ interactions: int) -> tp.Dict[str, tp.Any]:
+
+ data_dict: tp.Dict[str, tp.Any] = {
+ 'revision': revision.hash,
+ 'time_id': commit_map.short_time_id(revision),
+ 'base_hash': chash.hash,
+ 'interactions': interactions
+ }
+ return data_dict
+
+ result_data_dicts: tp.List[tp.Dict[str, tp.Any]] = []
+
+ for base_pair in base_inter_c_repo_pair_mapping:
+ inter_pair_amount_dict = base_inter_c_repo_pair_mapping[
+ base_pair]
+ interactions_amount = sum(inter_pair_amount_dict.values())
+ result_data_dicts.append(
+ build_dataframe_row(
+ chash=base_pair.commit.commit_hash,
+ interactions=interactions_amount
+ )
+ )
+ return pd.DataFrame(result_data_dicts
+ ), report.head_commit.hash, str(
+ report_path.stat().st_mtime_ns
+ )
+
+ report_files = get_processed_revisions_files(
+ project_name,
+ BlameReportExperiment,
+ file_name_filter=get_case_study_file_name_filter(case_study)
+ )
+
+ failed_report_files = get_failed_revisions_files(
+ project_name,
+ BlameReportExperiment,
+ file_name_filter=get_case_study_file_name_filter(case_study)
+ )
+
+ data_frame = build_cached_report_table(
+ cls.CACHE_ID, project_name, report_files, failed_report_files,
+ create_dataframe_layout, create_data_frame_for_report,
+ lambda path: path.report_filename.commit_hash.hash,
+ lambda path: str(path.stat().st_mtime_ns),
+ lambda a, b: int(a) > int(b)
+ )
+ return data_frame
diff --git a/varats/varats/data/databases/survivng_lines_database.py b/varats/varats/data/databases/survivng_lines_database.py
new file mode 100644
index 000000000..da2a68085
--- /dev/null
+++ b/varats/varats/data/databases/survivng_lines_database.py
@@ -0,0 +1,72 @@
+import typing as tp
+
+import pandas as pd
+from pygit2._pygit2 import GIT_SORT_TOPOLOGICAL
+
+from varats.data.cache_helper import load_cached_df_or_none, cache_dataframe
+from varats.data.databases.evaluationdatabase import EvaluationDatabase
+from varats.mapping.commit_map import CommitMap
+from varats.paper.case_study import CaseStudy
+from varats.project.project_util import get_local_project_git
+from varats.utils.git_util import (
+ calc_surviving_lines,
+ FullCommitHash,
+ ShortCommitHash,
+)
+
+
+class SurvivingLinesDatabase(
+ EvaluationDatabase,
+ cache_id="survivng_lines_data",
+ column_types={
+ "commit_hash": 'str',
+ "lines": 'int32'
+ }
+):
+
+ @classmethod
+ def _load_dataframe(
+ cls, project_name: str, commit_map: CommitMap,
+ case_study: tp.Optional[CaseStudy], **kwargs: tp.Dict[str, tp.Any]
+ ) -> pd.DataFrame:
+ data_frame = load_cached_df_or_none(
+ cls.CACHE_ID, project_name, cls.COLUMN_TYPES
+ )
+ project_repo = get_local_project_git(case_study.project_name)
+ revisions = case_study.revisions if case_study else [
+ FullCommitHash.from_pygit_commit(commit) for commit in
+ project_repo.walk(project_repo.head.target, GIT_SORT_TOPOLOGICAL)
+ ]
+ data_dicts: tp.List[tp.Dict[str, tp.Any]] = []
+ cached_revisions = data_frame.groupby("revision").groups.keys(
+ ) if data_frame is not None else set()
+ revisions_to_compute: tp.Set[str] = set(
+ map(lambda r: r.hash, revisions)
+ ) - cached_revisions
+
+ for revision in revisions_to_compute:
+ lines_per_commit = calc_surviving_lines(
+ case_study.project_name, ShortCommitHash(revision)
+ )
+
+ def build_dataframe_row(chash: FullCommitHash,
+ lines: int) -> tp.Dict[str, tp.Any]:
+ data_dict: tp.Dict[str, tp.Any] = {
+ 'revision': revision,
+ 'time_id': commit_map.time_id(FullCommitHash(revision)),
+ 'commit_hash': chash.hash,
+ 'lines': lines
+ }
+ return data_dict
+
+ for entry in lines_per_commit.items():
+ data_dicts.append(build_dataframe_row(entry[0], entry[1]))
+ if data_frame is None:
+ data_frame = pd.DataFrame(data_dicts)
+ else:
+ data_frame = pd.concat([data_frame,
+ pd.DataFrame(data_dicts)],
+ ignore_index=True,
+ copy=False)
+ cache_dataframe(cls.CACHE_ID, project_name, data_frame)
+ return data_frame
diff --git a/varats/varats/data/reports/blame_interaction_graph.py b/varats/varats/data/reports/blame_interaction_graph.py
index dc490b9b4..ece03b4f1 100644
--- a/varats/varats/data/reports/blame_interaction_graph.py
+++ b/varats/varats/data/reports/blame_interaction_graph.py
@@ -29,6 +29,7 @@
ChurnConfig,
UNCOMMITTED_COMMIT_HASH,
FullCommitHash,
+ CommitHash,
get_submodule_head,
)
@@ -85,6 +86,17 @@ class CAIGEdgeAttrs(TypedDict):
amount: int
+class FIGNodeAttrs(TypedDict):
+ """Funition interaction graph node attributes."""
+ function: tp.Optional[str]
+ num_commits: int
+
+
+class FIGEdgeAttrs(TypedDict):
+ """Function interaction graph edge attributes."""
+ amount: int
+
+
class InteractionGraph(abc.ABC):
"""Graph/Network built from interaction data."""
@@ -299,6 +311,64 @@ def commit_author_interaction_graph(
]["amount"] += data["amount"]
return caig
+ def function_interaction_graph(self):
+ """
+ Return a digraph with functions as nodes and interactions as edges.
+
+ Nodes can be referenced via their function name.
+ The graph has the following attributes:
+ Nodes:
+ - function: name of the function
+ - num_commits: number of commits aggregated in this node
+ Edges:
+ - amount: how often an interaction between two functions was found
+
+ Returns:
+ the author interaction graph
+ """
+ interaction_graph = self._interaction_graph()
+
+ def partition(node_u: BIGNodeTy, node_v: BIGNodeTy):
+ return node_u.function_name == node_v.function_name
+
+ def edge_data(
+ partition_a: tp.Set[BIGNodeTy], partition_b: tp.Set[BIGNodeTy]
+ ) -> FIGEdgeAttrs:
+ amount = 0
+ interactions: tp.List[tp.Tuple[CommitRepoPair, CommitRepoPair]] = []
+ for source in partition_a:
+ for sink in partition_b:
+ if interaction_graph.has_edge(source, sink):
+ amount += int(interaction_graph[source][sink]["amount"])
+ interactions.append((source.commit, sink.commit))
+
+ return {"amount": amount}
+
+ def node_data(nodes: tp.Set[BIGNodeTy]) -> FIGNodeAttrs:
+ functions = {
+ node.function_name if node.function_name else "Unknown"
+ for node in nodes
+ }
+ assert len(functions) == 1, "Some node has more then one function."
+ return {
+ "function": next(iter(functions)),
+ "num_commits": len(nodes)
+ }
+
+ fig = nx.quotient_graph(
+ interaction_graph,
+ partition=partition,
+ edge_data=edge_data,
+ node_data=node_data,
+ create_using=nx.DiGraph
+ )
+ relabel_dict: tp.Dict[tp.FrozenSet[BIGNodeTy], str] = {}
+ for node in fig.nodes:
+ relabel_dict[node] = tp.cast(AIGNodeAttrs,
+ fig.nodes[node])["function"]
+ nx.relabel_nodes(fig, relabel_dict, copy=False)
+ return fig
+
class BlameInteractionGraph(InteractionGraph):
"""Graph/Network built from blame interaction data."""
diff --git a/varats/varats/experiments/vara/blame_report_experiment.py b/varats/varats/experiments/vara/blame_report_experiment.py
index 747e8d313..d0da10fc3 100644
--- a/varats/varats/experiments/vara/blame_report_experiment.py
+++ b/varats/varats/experiments/vara/blame_report_experiment.py
@@ -27,7 +27,7 @@
from varats.experiment.wllvm import get_cached_bc_file_path, BCFileExtensions
from varats.project.project_util import get_local_project_git_paths
from varats.project.varats_project import VProject
-from varats.report.report import ReportSpecification
+from varats.report.report import ReportSpecification, ReportFilename
class BlameReportGeneration(actions.ProjectStep): # type: ignore
@@ -147,6 +147,10 @@ def actions_for_project(
return analysis_actions
+ @classmethod
+ def file_belongs_to_experiment(cls, file_name: ReportFilename) -> bool:
+ return file_name.experiment_shorthand in ["BRE", "BRER", "BRECIF"]
+
class BlameReportExperimentRegion(BlameReportExperiment, shorthand="BRER"):
"""Generates a blame report with region scoped taints."""
diff --git a/varats/varats/experiments/vara/feature_perf_runner.py b/varats/varats/experiments/vara/feature_perf_runner.py
index ad6c3b424..851aa05a0 100644
--- a/varats/varats/experiments/vara/feature_perf_runner.py
+++ b/varats/varats/experiments/vara/feature_perf_runner.py
@@ -20,6 +20,7 @@
from varats.report.tef_report import TEFReport
+
class FeaturePerfRunner(FeatureExperiment, shorthand="FPR"):
"""Test runner for feature performance."""
diff --git a/varats/varats/gui/cs_gen/case_study_generation.py b/varats/varats/gui/cs_gen/case_study_generation.py
index 7783f0132..efa83330d 100644
--- a/varats/varats/gui/cs_gen/case_study_generation.py
+++ b/varats/varats/gui/cs_gen/case_study_generation.py
@@ -7,6 +7,8 @@
import benchbuild as bb
import pygit2
+from benchbuild import Experiment
+from benchbuild.experiment import ExperimentRegistry
from PyQt5.QtCore import (
QModelIndex,
QDateTime,
@@ -17,7 +19,10 @@
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox
+import varats.paper.paper_config as PC
from varats.base.sampling_method import NormalSamplingMethod
+from varats.data.databases.file_status_database import FileStatusDatabase
+from varats.experiments.discover_experiments import initialize_experiments
from varats.gui.cs_gen.case_study_generation_ui import Ui_MainWindow
from varats.mapping.commit_map import get_commit_map, CommitMap
from varats.paper.case_study import CaseStudy, store_case_study
@@ -33,8 +38,10 @@
get_primary_project_source,
)
from varats.projects.discover_projects import initialize_projects
+from varats.report.report import FileStatusExtension
from varats.revision.revisions import is_revision_blocked
from varats.tools.research_tools.vara_manager import ProcessManager
+from varats.ts_utils.click_param_types import is_experiment_excluded
from varats.utils import settings
from varats.utils.git_util import (
get_initial_commit,
@@ -95,7 +102,16 @@ def __init__(self):
self.commit_search.textChanged.connect(
self.proxy_model.setFilterFixedString
)
+ self.cs_filter.stateChanged.connect(self.proxy_model.setCsFilter)
+ self.case_study.currentIndexChanged.connect(
+ self.proxy_model.update_case_study
+ )
self.show()
+ initialize_experiments()
+ self.experiment.addItems([
+ k for k, v in ExperimentRegistry.experiments.items()
+ if not is_experiment_excluded(k)
+ ])
def update_project_list(self, filter_string: str = "") -> None:
"""Update the project list when a filter is applied."""
@@ -202,6 +218,7 @@ def revisions_of_project(self) -> None:
GenerationStrategy.SELECT_REVISION.value
)
if self.selected_project != self.revision_list_project:
+ self.case_study.clear()
self.revision_details.setText("Loading Revisions")
self.revision_details.repaint()
# Update the local project git
@@ -224,8 +241,22 @@ def revisions_of_project(self) -> None:
cmap = get_commit_map(self.selected_project)
commit_model = CommitTableModel(
- list(map(commit_lookup_helper, commits)), cmap, project
+ list(map(commit_lookup_helper, commits)), cmap, project,
+ ExperimentRegistry.experiments[self.experiment.currentText()]
+ )
+ self.proxy_model.setProject(project)
+ self.case_study.currentIndexChanged.connect(
+ commit_model.update_case_study
)
+ self.experiment.currentTextChanged.connect(
+ commit_model.update_experiment
+ )
+ current_config = PC.get_paper_config()
+ case_studies = current_config.get_all_case_studies()
+ self.case_study.addItems([
+ f"{cs.project_name}_{cs.version}" for cs in case_studies
+ if cs.project_name == self.selected_project
+ ])
self.proxy_model.setSourceModel(commit_model)
self.revision_list_project = self.selected_project
self.revision_details.clear()
@@ -246,22 +277,39 @@ def show_revision_data(self, index: QModelIndex) -> None:
class CommitTableFilterModel(QSortFilterProxyModel):
"""Filter Model for the revision table."""
filter_string = ""
+ cs_filter = False
def setFilterFixedString(self, pattern: str) -> None:
self.filter_string = pattern
self.invalidate()
+ def update_case_study(self, index: int) -> None:
+ current_config = PC.get_paper_config()
+ case_studies = [
+ cs for cs in current_config.get_all_case_studies()
+ if cs.project_name == self._project.NAME
+ ]
+ self._case_study = case_studies[index]
+ self.invalidate()
+
+ def setProject(self, project: tp.Type['bb.Project']) -> None:
+ self._project = project
+
+ def setCsFilter(self, cs_filter: bool) -> None:
+ self.cs_filter = cs_filter
+ self.invalidate()
+
def filterAcceptsRow(
self, source_row: int, source_parent: QModelIndex
) -> bool:
commit_index = self.sourceModel().index(source_row, 0, source_parent)
author_index = self.sourceModel().index(source_row, 1, source_parent)
- return self.sourceModel().data(commit_index,
+ return ((not self.cs_filter) or FullCommitHash(self.sourceModel().data(commit_index,Qt.WhatsThisRole).hex) in self._case_study.revisions) and (self.sourceModel().data(commit_index,
Qt.DisplayRole).lower() \
.__contains__(self.filter_string.lower()) \
or self.sourceModel().data(author_index,
Qt.DisplayRole).lower() \
- .__contains__(self.filter_string.lower())
+ .__contains__(self.filter_string.lower()))
class CommitTableModel(QAbstractTableModel):
@@ -270,13 +318,51 @@ class CommitTableModel(QAbstractTableModel):
def __init__(
self, data: tp.List[pygit2.Commit], cmap: CommitMap,
- project: tp.Type['bb.Project']
+ project: tp.Type['bb.Project'], experiment_type: tp.Type[Experiment]
):
super().__init__()
self._project = project
self._data = data
+ self._case_study: tp.Optional[CaseStudy] = None
+ self._experiment_type = experiment_type
self._cmap = cmap
+ def update_case_study(self, index: int) -> None:
+ current_config = PC.get_paper_config()
+ case_studies = [
+ cs for cs in current_config.get_all_case_studies()
+ if cs.project_name == self._project.NAME
+ ]
+ self._case_study = case_studies[index]
+ if self._experiment_type:
+ self._status_data = FileStatusDatabase.get_data_for_project(
+ self._case_study.project_name, ["revision", "file_status"],
+ self._cmap,
+ self._case_study,
+ experiment_type=self._experiment_type,
+ tag_blocked=False
+ )
+ self._status_data.set_index("revision", inplace=True)
+ self.dataChanged.emit(
+ self.index(0, 0), self.index(self.rowCount(), self.columnCount())
+ )
+
+ def update_experiment(self, index: str) -> None:
+ self._experiment_type = ExperimentRegistry.experiments[index]
+ if self._case_study:
+ self._status_data = FileStatusDatabase.get_data_for_project(
+ self._case_study.project_name, ["revision", "file_status"],
+ self._cmap,
+ self._case_study,
+ experiment_type=self._experiment_type,
+ tag_blocked=False
+ )
+ self._status_data.set_index("revision", inplace=True)
+
+ self.dataChanged.emit(
+ self.index(0, 0), self.index(self.rowCount(), self.columnCount())
+ )
+
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.header_labels[section]
@@ -311,6 +397,28 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> tp.Any:
return QColor(50, 100, 255)
if role == Qt.ToolTipRole:
return "Blocked"
+ if self._case_study and self._experiment_type:
+ if role == Qt.ForegroundRole:
+ chash = ShortCommitHash(commit.hex)
+ if chash in self._status_data.index:
+ if self._status_data.loc[
+ chash, "file_status"
+ ] == FileStatusExtension.SUCCESS.get_status_extension():
+ return QColor(0, 255, 0)
+ elif self._status_data.loc[
+ chash, "file_status"
+ ] == FileStatusExtension.FAILED.get_status_extension():
+ return QColor(255, 0, 0)
+ elif self._status_data.loc[
+ chash, "file_status"
+ ] == FileStatusExtension.COMPILE_ERROR.get_status_extension(
+ ):
+ return QColor(255, 0, 0)
+ elif self._status_data.loc[
+ chash, "file_status"
+ ] == FileStatusExtension.MISSING.get_status_extension():
+ return QColor(255, 255, 0)
+
if role == Qt.WhatsThisRole:
return commit
diff --git a/varats/varats/gui/cs_gen/case_study_generation_ui.py b/varats/varats/gui/cs_gen/case_study_generation_ui.py
index 8b027219e..100bdfa72 100644
--- a/varats/varats/gui/cs_gen/case_study_generation_ui.py
+++ b/varats/varats/gui/cs_gen/case_study_generation_ui.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file '../uicomponents/CaseStudyGeneration.ui'
+# Form implementation generated from reading ui file 'VaRA-Tool-Suite/uicomponents/CaseStudyGeneration.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
#
@@ -14,7 +14,7 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.setEnabled(True)
- MainWindow.resize(760, 443)
+ MainWindow.resize(760, 491)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding
@@ -87,6 +87,15 @@ def setupUi(self, MainWindow):
self.revisions.setSizePolicy(sizePolicy)
self.revisions.setObjectName("revisions")
self.verticalLayout_3.addWidget(self.revisions)
+ self.case_study = QtWidgets.QComboBox(self.revisionsPage)
+ self.case_study.setObjectName("case_study")
+ self.verticalLayout_3.addWidget(self.case_study)
+ self.experiment = QtWidgets.QComboBox(self.revisionsPage)
+ self.experiment.setObjectName("experiment")
+ self.verticalLayout_3.addWidget(self.experiment)
+ self.cs_filter = QtWidgets.QCheckBox(self.revisionsPage)
+ self.cs_filter.setObjectName("cs_filter")
+ self.verticalLayout_3.addWidget(self.cs_filter)
self.commit_search = QtWidgets.QLineEdit(self.revisionsPage)
self.commit_search.setClearButtonEnabled(True)
self.commit_search.setObjectName("commit_search")
@@ -206,7 +215,7 @@ def setupUi(self, MainWindow):
self.gridLayout.addWidget(self.projects, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect(0, 0, 760, 22))
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 760, 34))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
@@ -229,6 +238,7 @@ def retranslateUi(self, MainWindow):
self.label_2.setText(_translate("MainWindow", "Casestudy Version"))
self.generate.setText(_translate("MainWindow", "Generate"))
self.revisions.setText(_translate("MainWindow", "Revisions"))
+ self.cs_filter.setText(_translate("MainWindow", "Filter CaseStudy´"))
self.commit_search.setPlaceholderText(
_translate("MainWindow", "Search")
)
@@ -253,8 +263,8 @@ def retranslateUi(self, MainWindow):
"\n"
"
\n"
- "
"
+ "\n"
+ "