From 7e1a83dda1f63deb8560ccdac40406398adbfd4b Mon Sep 17 00:00:00 2001 From: Sebastian Simon Date: Wed, 9 Oct 2024 16:14:01 +0200 Subject: [PATCH] Add GitHub Action plugin and tests --- .../config_types/config_type_inferer.py | 8 +-- .../plugins/concept/github_actions_plugin.py | 47 +++++++++++++++++ src/cfgnet/plugins/file_type/yaml_plugin.py | 19 +++---- src/cfgnet/plugins/plugin_manager.py | 2 + .../concept/test_ansible_playbook_plugin.py | 30 +++++------ .../plugins/concept/test_circleci_plugin.py | 28 +++++----- .../concept/test_github_action_plugin.py | 51 +++++++++++++++++++ tests/cfgnet/plugins/test_plugin_manager.py | 4 +- tests/files/.github/workflows/ci.yml | 30 +++++++++++ 9 files changed, 176 insertions(+), 43 deletions(-) create mode 100644 src/cfgnet/plugins/concept/github_actions_plugin.py create mode 100644 tests/cfgnet/plugins/concept/test_github_action_plugin.py create mode 100644 tests/files/.github/workflows/ci.yml diff --git a/src/cfgnet/config_types/config_type_inferer.py b/src/cfgnet/config_types/config_type_inferer.py index 08723e4..63cf5e6 100644 --- a/src/cfgnet/config_types/config_type_inferer.py +++ b/src/cfgnet/config_types/config_type_inferer.py @@ -84,7 +84,7 @@ class ConfigTypeInferer: @staticmethod def is_boolean(value: str) -> bool: - return bool(re.match(ConfigTypeInferer.regex_boolean, value)) + return bool(re.fullmatch(ConfigTypeInferer.regex_boolean, value)) # pylint: disable=too-many-return-statements @staticmethod @@ -95,6 +95,9 @@ def get_config_type( # noqa: C901 # Check option name and value against types for which an option name and value regex exists. option_name = option_name.split(".")[-1] + if ConfigTypeInferer.is_boolean(value): + return ConfigType.BOOLEAN + if bool( re.match(ConfigTypeInferer.regex_port_option, option_name) ) and bool(re.fullmatch(ConfigTypeInferer.regex_port_value, value)): @@ -192,9 +195,6 @@ def get_config_type( # noqa: C901 if bool(re.fullmatch(ConfigTypeInferer.regex_number, value)): return ConfigType.NUMBER - if bool(re.fullmatch(ConfigTypeInferer.regex_boolean, value)): - return ConfigType.BOOLEAN - if bool(re.fullmatch(ConfigTypeInferer.regex_size_value, value)): return ConfigType.SIZE diff --git a/src/cfgnet/plugins/concept/github_actions_plugin.py b/src/cfgnet/plugins/concept/github_actions_plugin.py new file mode 100644 index 0000000..24a414b --- /dev/null +++ b/src/cfgnet/plugins/concept/github_actions_plugin.py @@ -0,0 +1,47 @@ +# This file is part of the CfgNet module. +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . +import re +from cfgnet.plugins.file_type.yaml_plugin import YAMLPlugin +from cfgnet.config_types.config_types import ConfigType + + +class GitHubActionPlugin(YAMLPlugin): + file_name = re.compile(r".*?\.github\/workflows\/[^\/]*\.yml$") + + def __init__(self): + super().__init__("github-action") + + def is_responsible(self, abs_file_path): + if self.file_name.search(abs_file_path): + return True + return False + + def get_config_type(self, option_name: str) -> ConfigType: + """ + Find config type based on option name. + + :param option_name: name of option + :return: config type + """ + if option_name in ("run"): + return ConfigType.COMMAND + + if option_name in ("name", "uses"): + return ConfigType.NAME + + if option_name in ("python-version"): + return ConfigType.VERSION_NUMBER + + return ConfigType.UNKNOWN diff --git a/src/cfgnet/plugins/file_type/yaml_plugin.py b/src/cfgnet/plugins/file_type/yaml_plugin.py index 8956a23..26758c3 100644 --- a/src/cfgnet/plugins/file_type/yaml_plugin.py +++ b/src/cfgnet/plugins/file_type/yaml_plugin.py @@ -80,17 +80,18 @@ def _parse_mapping_node(self, node, parent): self._iter_tree(child, parent) def _parse_sequence_node(self, node, parent): - index = 0 + # index = 0 for child in node.value: if isinstance(child, MappingNode): - offset_option = OptionNode( - "offset:" + str(index), - node.start_mark.line + 1, - ) - parent.add_child(offset_option) - - self._iter_tree(child, offset_option) - index += 1 + # offset_option = OptionNode( + # "offset:" + str(index), + # node.start_mark.line + 1, + # ) + # parent.add_child(offset_option) + + # self._iter_tree(child, offset_option) + self._iter_tree(child, parent) + # index += 1 else: self._iter_tree(child, parent) diff --git a/src/cfgnet/plugins/plugin_manager.py b/src/cfgnet/plugins/plugin_manager.py index bef4f9d..150e4c3 100644 --- a/src/cfgnet/plugins/plugin_manager.py +++ b/src/cfgnet/plugins/plugin_manager.py @@ -53,6 +53,7 @@ from cfgnet.plugins.concept.mapreduce_plugin import MapReducePlugin from cfgnet.plugins.concept.circleci_plugin import CircleCiPlugin from cfgnet.plugins.concept.cargo_plugin import CargoPlugin +from cfgnet.plugins.concept.github_actions_plugin import GitHubActionPlugin class PluginManager: @@ -87,6 +88,7 @@ class PluginManager: MapReducePlugin(), CircleCiPlugin(), CargoPlugin(), + GitHubActionPlugin(), ] file_type_plugins: List[Plugin] = [ diff --git a/tests/cfgnet/plugins/concept/test_ansible_playbook_plugin.py b/tests/cfgnet/plugins/concept/test_ansible_playbook_plugin.py index 25c84f1..b1f7729 100644 --- a/tests/cfgnet/plugins/concept/test_ansible_playbook_plugin.py +++ b/tests/cfgnet/plugins/concept/test_ansible_playbook_plugin.py @@ -62,15 +62,15 @@ def test_parse_ansible_playbook_file(get_plugin): assert artifact is not None assert len(nodes) == 34 - assert make_id("playbook.yml", "offset:0", "remote_user", "root") in ids - assert make_id("playbook.yml", "offset:0", "tasks", "offset:0", "ansible.builtin.yum", "name", "httpd") in ids - assert make_id("playbook.yml", "offset:2", "tasks", "offset:0", "win_get_url", "url", "https://test.html") in ids - assert make_id("playbook.yml", "offset:3", "tasks", "offset:0", "win_user", "password", "test123") in ids - assert make_id("playbook.yml", "offset:3", "tasks", "offset:0", "win_user", "state", "present") in ids - assert make_id("playbook.yml", "offset:4", "tasks", "offset:0", "ansible.builtin.git", "dest", "/home/www") in ids - assert make_id("playbook.yml", "offset:4", "tasks", "offset:0", "ansible.builtin.git", "accept_hostkey", "true") in ids - assert make_id("playbook.yml", "offset:4", "tasks", "offset:0", "ansible.builtin.git", "version", "master") in ids - assert make_id("playbook.yml", "offset:4", "hosts", "localhost") in ids + assert make_id("playbook.yml", "remote_user", "root") in ids + assert make_id("playbook.yml", "tasks", "ansible.builtin.yum", "name", "httpd") in ids + assert make_id("playbook.yml", "tasks", "win_get_url", "url", "https://test.html") in ids + assert make_id("playbook.yml", "tasks", "win_user", "password", "test123") in ids + assert make_id("playbook.yml", "tasks", "win_user", "state", "present") in ids + assert make_id("playbook.yml", "tasks", "ansible.builtin.git", "dest", "/home/www") in ids + assert make_id("playbook.yml", "tasks", "ansible.builtin.git", "accept_hostkey", "true") in ids + assert make_id("playbook.yml", "tasks", "ansible.builtin.git", "version", "master") in ids + assert make_id("playbook.yml", "hosts", "localhost") in ids def test_config_types(get_plugin): @@ -79,12 +79,12 @@ def test_config_types(get_plugin): artifact = ansible_playbook_plugin.parse_file(playbook_file, "playbook.yml") nodes = artifact.get_nodes() - path_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:4", "tasks", "offset:0", "ansible.builtin.git", "dest", "/home/www"), nodes)) - name_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:0", "tasks", "offset:0", "ansible.builtin.yum", "name", "httpd"), nodes)) - state_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:3", "tasks", "offset:0", "win_user", "state", "present"), nodes)) - url_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:2", "tasks", "offset:0", "win_get_url", "url", "https://test.html"), nodes)) - password_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:3", "tasks", "offset:0", "win_user", "password", "test123"), nodes)) - host_node = next(filter(lambda x: x.id == make_id("playbook.yml", "offset:4", "hosts", "localhost"), nodes)) + path_node = next(filter(lambda x: x.id == make_id("playbook.yml", "tasks", "ansible.builtin.git", "dest", "/home/www"), nodes)) + name_node = next(filter(lambda x: x.id == make_id("playbook.yml", "tasks", "ansible.builtin.yum", "name", "httpd"), nodes)) + state_node = next(filter(lambda x: x.id == make_id("playbook.yml", "tasks", "win_user", "state", "present"), nodes)) + url_node = next(filter(lambda x: x.id == make_id("playbook.yml", "tasks", "win_get_url", "url", "https://test.html"), nodes)) + password_node = next(filter(lambda x: x.id == make_id("playbook.yml", "tasks", "win_user", "password", "test123"), nodes)) + host_node = next(filter(lambda x: x.id == make_id("playbook.yml", "hosts", "localhost"), nodes)) assert path_node.config_type == ConfigType.PATH assert name_node.config_type == ConfigType.NAME diff --git a/tests/cfgnet/plugins/concept/test_circleci_plugin.py b/tests/cfgnet/plugins/concept/test_circleci_plugin.py index cbf7ae7..f061216 100644 --- a/tests/cfgnet/plugins/concept/test_circleci_plugin.py +++ b/tests/cfgnet/plugins/concept/test_circleci_plugin.py @@ -55,20 +55,20 @@ def test_parse_circle_file(get_plugin): assert len(nodes) == 16 assert make_id("config.yml", "file", "config.yml") in ids - assert make_id("config.yml", "jobs", "build", "docker", "offset:0", "image", "circleci/node:14") in ids + assert make_id("config.yml", "jobs", "build", "docker", "image", "circleci/node:14") in ids assert make_id("config.yml", "jobs", "build", "steps", "checkout") in ids - assert make_id("config.yml", "jobs", "build", "steps", "offset:0", "run", "name", "Install dependencies") in ids - assert make_id("config.yml", "jobs", "build", "steps", "offset:0", "run", "command", "npm install") in ids - assert make_id("config.yml", "jobs", "build", "steps", "offset:1", "persist_to_workspace", "root", ".") in ids - assert make_id("config.yml", "jobs", "build", "steps", "offset:1", "persist_to_workspace", "paths", "dist") in ids - assert make_id("config.yml", "jobs", "build", "steps", "offset:1", "persist_to_workspace", "paths", "src") in ids - assert make_id("config.yml", "jobs", "deploy", "docker", "offset:0", "image", "circleci/node:14") in ids - assert make_id("config.yml", "jobs", "deploy", "steps", "offset:0", "attach_workspace", "at", "/workspace") in ids - assert make_id("config.yml", "jobs", "deploy", "steps", "offset:1", "run", "name", "Deploy application") in ids - assert make_id("config.yml", "jobs", "deploy", "steps", "offset:1", "run", "command", 'echo "Deploying application..."') in ids + assert make_id("config.yml", "jobs", "build", "steps", "run", "name", "Install dependencies") in ids + assert make_id("config.yml", "jobs", "build", "steps", "run", "command", "npm install") in ids + assert make_id("config.yml", "jobs", "build", "steps", "persist_to_workspace", "root", ".") in ids + assert make_id("config.yml", "jobs", "build", "steps", "persist_to_workspace", "paths", "dist") in ids + assert make_id("config.yml", "jobs", "build", "steps", "persist_to_workspace", "paths", "src") in ids + assert make_id("config.yml", "jobs", "deploy", "docker", "image", "circleci/node:14") in ids + assert make_id("config.yml", "jobs", "deploy", "steps", "attach_workspace", "at", "/workspace") in ids + assert make_id("config.yml", "jobs", "deploy", "steps", "run", "name", "Deploy application") in ids + assert make_id("config.yml", "jobs", "deploy", "steps", "run", "command", 'echo "Deploying application..."') in ids assert make_id("config.yml", "workflows", "version", "2") in ids assert make_id("config.yml", "workflows", "build_and_deploy", "jobs", "build") in ids - assert make_id("config.yml", "workflows", "build_and_deploy", "jobs", "offset:0", "deploy", "requires", "build") in ids + assert make_id("config.yml", "workflows", "build_and_deploy", "jobs", "deploy", "requires", "build") in ids def test_config_types(get_plugin): @@ -79,9 +79,9 @@ def test_config_types(get_plugin): nodes = artifact.get_nodes() version_node = next(filter(lambda x: x.id == make_id("config.yml", "workflows", "version", "2"), nodes)) - command_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "build", "steps", "offset:0", "run", "command", "npm install"), nodes)) - path_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "deploy", "steps", "offset:0", "attach_workspace", "at", "/workspace"), nodes)) - image_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "build", "docker", "offset:0", "image", "circleci/node:14"), nodes)) + command_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "build", "steps", "run", "command", "npm install"), nodes)) + path_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "deploy", "steps", "attach_workspace", "at", "/workspace"), nodes)) + image_node = next(filter(lambda x: x.id == make_id("config.yml", "jobs", "build", "docker", "image", "circleci/node:14"), nodes)) assert version_node.config_type == ConfigType.VERSION_NUMBER assert command_node.config_type == ConfigType.COMMAND diff --git a/tests/cfgnet/plugins/concept/test_github_action_plugin.py b/tests/cfgnet/plugins/concept/test_github_action_plugin.py new file mode 100644 index 0000000..54542d0 --- /dev/null +++ b/tests/cfgnet/plugins/concept/test_github_action_plugin.py @@ -0,0 +1,51 @@ +# This file is part of the CfgNet module. +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + +import os +import pytest + +from cfgnet.plugins.concept.github_actions_plugin import GitHubActionPlugin +from cfgnet.config_types.config_types import ConfigType +from tests.utility.id_creator import make_id + + +@pytest.fixture(name="get_plugin") +def get_plugin_(): + plugin = GitHubActionPlugin() + return plugin + + +def test_is_responsible(get_plugin): + plugin = get_plugin + + assert plugin.is_responsible("tests/files/.github/workflows/ci.yml") + assert not plugin.is_responsible("tests/files/test.yml") + + +def test_config_types(get_plugin): + plugin = get_plugin + file_name = os.path.abspath("tests/files/.github/workflows/ci.yml") + artifact = plugin.parse_file(file_name, "ci.yml") + nodes = artifact.get_nodes() + + name_node = next(filter(lambda x: x.id == make_id("ci.yml", "name", "Code Quality"), nodes)) + version_node = next(filter(lambda x: x.id == make_id("ci.yml", "jobs", "code_style", "steps", "with", "python-version", "3.9"), nodes)) + command_node = next(filter(lambda x: x.id == make_id("ci.yml", "jobs", "code_style", "steps", "run", "poetry install"), nodes)) + boolean_node = next(filter(lambda x: x.id == make_id("ci.yml", "jobs", "code_style", "steps", "with", "virtualenvs-create", "true"), nodes)) + + assert name_node.config_type == ConfigType.NAME + assert version_node.config_type == ConfigType.VERSION_NUMBER + assert command_node.config_type == ConfigType.COMMAND + assert boolean_node.config_type == ConfigType.BOOLEAN diff --git a/tests/cfgnet/plugins/test_plugin_manager.py b/tests/cfgnet/plugins/test_plugin_manager.py index b4683cd..36b6e1b 100644 --- a/tests/cfgnet/plugins/test_plugin_manager.py +++ b/tests/cfgnet/plugins/test_plugin_manager.py @@ -19,7 +19,7 @@ def test_get_all_plugins(): all_plugins = PluginManager.get_plugins() - assert len(all_plugins) == 28 + assert len(all_plugins) == 29 def test_get_responsible_plugin(): @@ -53,6 +53,7 @@ def test_get_responsible_plugin(): mapreduce_plugin = PluginManager.get_responsible_plugin(plugins, "path/to/mapred-site.xml") circleci_plugin = PluginManager.get_responsible_plugin(plugins, "path/to/.circleci/config.yml") cargo_plugin = PluginManager.get_responsible_plugin(plugins, "path/to/Cargo.toml") + github_action_plugin = PluginManager.get_responsible_plugin(plugins, ".github/workflows/test.yml") assert docker_plugin.concept_name == "docker" assert maven_plugin.concept_name == "maven" @@ -82,3 +83,4 @@ def test_get_responsible_plugin(): assert mapreduce_plugin.concept_name == "mapreduce" assert circleci_plugin.concept_name == "circleci" assert cargo_plugin.concept_name == "cargo" + assert github_action_plugin.concept_name == "github-action" diff --git a/tests/files/.github/workflows/ci.yml b/tests/files/.github/workflows/ci.yml new file mode 100644 index 0000000..059d122 --- /dev/null +++ b/tests/files/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Code Quality + +on: [push] + +jobs: + + code_style: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install Dependencies + run: poetry install + if: steps.cache.outputs.cache-hit != 'true' + + - name: Run linters + run: make linter