Mercurial > hg > CbC > CbC_llvm
diff clang/utils/analyzer/ProjectMap.py @ 207:2e18cbf3894f
LLVM12
author | Shinji KONO <kono@ie.u-ryukyu.ac.jp> |
---|---|
date | Tue, 08 Jun 2021 06:07:14 +0900 |
parents | |
children | 1f2b6ac9f198 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/clang/utils/analyzer/ProjectMap.py Tue Jun 08 06:07:14 2021 +0900 @@ -0,0 +1,208 @@ +import json +import os + +from enum import auto, Enum +from typing import Any, Dict, List, NamedTuple, Optional, Tuple + + +JSON = Dict[str, Any] + + +DEFAULT_MAP_FILE = "projects.json" + + +class DownloadType(str, Enum): + GIT = "git" + ZIP = "zip" + SCRIPT = "script" + + +class Size(int, Enum): + """ + Size of the project. + + Sizes do not directly correspond to the number of lines or files in the + project. The key factor that is important for the developers of the + analyzer is the time it takes to analyze the project. Here is how + the following sizes map to times: + + TINY: <1min + SMALL: 1min-10min + BIG: 10min-1h + HUGE: >1h + + The borders are a bit of a blur, especially because analysis time varies + from one machine to another. However, the relative times will stay pretty + similar, and these groupings will still be helpful. + + UNSPECIFIED is a very special case, which is intentionally last in the list + of possible sizes. If the user wants to filter projects by one of the + possible sizes, we want projects with UNSPECIFIED size to be filtered out + for any given size. + """ + TINY = auto() + SMALL = auto() + BIG = auto() + HUGE = auto() + UNSPECIFIED = auto() + + @staticmethod + def from_str(raw_size: Optional[str]) -> "Size": + """ + Construct a Size object from an optional string. + + :param raw_size: optional string representation of the desired Size + object. None will produce UNSPECIFIED size. + + This method is case-insensitive, so raw sizes 'tiny', 'TINY', and + 'TiNy' will produce the same result. + """ + if raw_size is None: + return Size.UNSPECIFIED + + raw_size_upper = raw_size.upper() + # The implementation is decoupled from the actual values of the enum, + # so we can easily add or modify it without bothering about this + # function. + for possible_size in Size: + if possible_size.name == raw_size_upper: + return possible_size + + possible_sizes = [size.name.lower() for size in Size + # no need in showing our users this size + if size != Size.UNSPECIFIED] + raise ValueError(f"Incorrect project size '{raw_size}'. " + f"Available sizes are {possible_sizes}") + + +class ProjectInfo(NamedTuple): + """ + Information about a project to analyze. + """ + name: str + mode: int + source: DownloadType = DownloadType.SCRIPT + origin: str = "" + commit: str = "" + enabled: bool = True + size: Size = Size.UNSPECIFIED + + def with_fields(self, **kwargs) -> "ProjectInfo": + """ + Create a copy of this project info with customized fields. + NamedTuple is immutable and this is a way to create modified copies. + + info.enabled = True + info.mode = 1 + + can be done as follows: + + modified = info.with_fields(enbled=True, mode=1) + """ + return ProjectInfo(**{**self._asdict(), **kwargs}) + + +class ProjectMap: + """ + Project map stores info about all the "registered" projects. + """ + def __init__(self, path: Optional[str] = None, should_exist: bool = True): + """ + :param path: optional path to a project JSON file, when None defaults + to DEFAULT_MAP_FILE. + :param should_exist: flag to tell if it's an exceptional situation when + the project file doesn't exist, creates an empty + project list instead if we are not expecting it to + exist. + """ + if path is None: + path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE) + + if not os.path.exists(path): + if should_exist: + raise ValueError( + f"Cannot find the project map file {path}" + f"\nRunning script for the wrong directory?\n") + else: + self._create_empty(path) + + self.path = path + self._load_projects() + + def save(self): + """ + Save project map back to its original file. + """ + self._save(self.projects, self.path) + + def _load_projects(self): + with open(self.path) as raw_data: + raw_projects = json.load(raw_data) + + if not isinstance(raw_projects, list): + raise ValueError( + "Project map should be a list of JSON objects") + + self.projects = self._parse(raw_projects) + + @staticmethod + def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]: + return [ProjectMap._parse_project(raw_project) + for raw_project in raw_projects] + + @staticmethod + def _parse_project(raw_project: JSON) -> ProjectInfo: + try: + name: str = raw_project["name"] + build_mode: int = raw_project["mode"] + enabled: bool = raw_project.get("enabled", True) + source: DownloadType = raw_project.get("source", "zip") + size = Size.from_str(raw_project.get("size", None)) + + if source == DownloadType.GIT: + origin, commit = ProjectMap._get_git_params(raw_project) + else: + origin, commit = "", "" + + return ProjectInfo(name, build_mode, source, origin, commit, + enabled, size) + + except KeyError as e: + raise ValueError( + f"Project info is required to have a '{e.args[0]}' field") + + @staticmethod + def _get_git_params(raw_project: JSON) -> Tuple[str, str]: + try: + return raw_project["origin"], raw_project["commit"] + except KeyError as e: + raise ValueError( + f"Profect info is required to have a '{e.args[0]}' field " + f"if it has a 'git' source") + + @staticmethod + def _create_empty(path: str): + ProjectMap._save([], path) + + @staticmethod + def _save(projects: List[ProjectInfo], path: str): + with open(path, "w") as output: + json.dump(ProjectMap._convert_infos_to_dicts(projects), + output, indent=2) + + @staticmethod + def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]: + return [ProjectMap._convert_info_to_dict(project) + for project in projects] + + @staticmethod + def _convert_info_to_dict(project: ProjectInfo) -> JSON: + whole_dict = project._asdict() + defaults = project._field_defaults + + # there is no need in serializing fields with default values + for field, default_value in defaults.items(): + if whole_dict[field] == default_value: + del whole_dict[field] + + return whole_dict