diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..aa35841 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,24 @@ +--- +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +permissions: read-all + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + - name: setup-python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: install-dependencies + run: pip install -r pypi_bumpversion_check/requirements.txt + - name: pre-commit-run + uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..bf35e65 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,35 @@ +--- +name: Publish Python 🐍 distributions 📦 to PyPI + +on: + pull_request: + branches: + - main + types: [closed] + +jobs: + build-n-publish: + if: ${{ github.event.pull_request.merged }} + name: Build and publish Python 🐍 distributions 📦 to PyPI + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: 3.11 + + - name: Install pip packages + run: pip install twine build setuptools + + - name: Build the package + run: python -m build + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..dac0169 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,74 @@ +--- +name: tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + # ---------------------- + # JOB 1: Run unit tests + # ---------------------- + tests-unit: + name: tests-unit + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: 3.11 + + - name: Run tests + run: python3 -m unittest tests/test_main.py + # ---------------------- + # JOB 2: Run python package end to end test + # ---------------------- + test-package-e2e: + name: test-package-e2e + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: 3.11 + + - name: Run test-package-e2e.sh + run: | + cd tests-package-e2e + ./test-package-e2e.sh + # ---------------------- + # JOB 3: Run pre-commit hook test + # ---------------------- + test-pre-commit-hook: + name: test-pre-commit-hook + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: 3.11 + + - name: Run test-pre-commit-hook.sh + run: | + pip install pre-commit + cd tests-pre-commit-hook + ./test-pre-commit-hook.sh diff --git a/.gitignore b/.gitignore index 82f9275..b2ce492 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ # Byte-compiled / optimized / DLL files __pycache__/ +**/__pycache__/ *.py[cod] *$py.class +.DS_Store +**/.DS_Store + +**/precommit-e2e.test + # C extensions *.so @@ -85,7 +91,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..270b636 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,92 @@ +--- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-merge-conflict + - id: check-added-large-files + args: [--maxkb=500] + - id: trailing-whitespace + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: file-contents-sorter + - id: check-case-conflict + - id: mixed-line-ending + args: [--fix=lf] + # ----------------------------- + # Checkov is a static code analysis tool for scanning infrastructure as code (IaC) files for misconfigurations + # that may lead to security or compliance problems. + # ----------------------------- + # Checkov includes more than 750 predefined policies to check for common misconfiguration issues. + # Checkov also supports the creation and contribution of custom policies. + # https://www.checkov.io/4.Integrations/pre-commit.html + # ----------------------------- + - repo: https://github.com/bridgecrewio/checkov.git + rev: 3.2.141 + hooks: + - id: checkov + # ----------------------------- + # Gitleaks SAST tool for detecting and preventing hardcoded secrets like passwords, api keys, and tokens in git repos + # ----------------------------- + # If you are knowingly committing something that is not a secret and gitleaks is catching it, + # you can add an inline comment of '# gitleaks:allow' to the end of that line in your file. + # This will instructs gitleaks to ignore that secret - example: + # some_non_secret_value = a1b2c3d4e5f6g7h8i9j0 # gitleaks:allow + # ----------------------------- + - repo: https://github.com/gitleaks/gitleaks + rev: v8.18.4 + hooks: + - id: gitleaks + # ----------------------------- + # Generates Table of Contents in Markdown files + # ----------------------------- + - repo: https://github.com/frnmst/md-toc + rev: 9.0.0 + hooks: + - id: md-toc + args: [-p, github] # CLI options + # ----------------------------- + # YAML Linting on yaml files for pre-commit and github actions + # ----------------------------- + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + name: Check YAML syntax with yamllint + args: [--strict, -c=.yamllint.yaml, '.'] + always_run: true + pass_filenames: true + # ----------------------------- + # Install PYPI bumpversion check requirements + # ----------------------------- + - repo: local + hooks: + - id: install-pypi_bumpversion_check-requirements + name: Install PYPI bumpversion check requirements + entry: pip install -r pypi_bumpversion_check/requirements.txt + language: system + files: pyproject.toml + # ----------------------------- + # PYPI bumpversion check + # ----------------------------- + - repo: local + hooks: + - id: pypi_bumpversion_check + name: Check version + entry: python pypi_bumpversion_check/check_version.py + language: system + files: pyproject.toml + # ----------------------------- + # Unit Tests + # ----------------------------- + - repo: local + hooks: + - id: unittest + name: Run unit tests + entry: python -m unittest tests.test_main + language: system + pass_filenames: false + always_run: true diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..08472bf --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +--- +- id: find-and-replace-strings + name: find-and-replace-strings + description: Finds strings in files and replaces them with other strings. + entry: find-and-replace-strings + language: python diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..bb6d180 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,35 @@ +--- +yaml-files: + - '*.yaml' + - '*.yml' + - '.yamllint' + +rules: + anchors: enable + braces: enable + brackets: enable + colons: enable + commas: enable + comments: + level: warning + comments-indentation: + level: warning + document-end: disable + document-start: + level: warning + empty-lines: enable + empty-values: disable + float-values: disable + hyphens: enable + indentation: enable + key-duplicates: enable + key-ordering: disable + line-length: + max: 120 + level: warning + new-line-at-end-of-file: enable + new-lines: enable + octal-values: disable + quoted-strings: disable + trailing-spaces: enable + truthy: disable diff --git a/README.md b/README.md index ad74a94..faa9a47 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,200 @@ # find-and-replace -Python package and pre-commit-hook for finding and replacing string(s) in file(s) + +Python package and pre-commit-hook for finding and replacing string(s) in file(s). + +## Prerequisite + +pre-commit install +pre-commit install -t pre-push + +The above will make sure precommit will be run automatically on push + +## Installation as a pip package + +This is an easy to use package which is already available here https://pypi.org/project/find-and-replace-template-commit-check/: + +![package to use](./assets/pypi-package.png "Title") + +You can install the package via pip: + +```bash +pip install find-and-replace-strings +``` +In case if you want to use it from the root folder in source: + +``` + python -m find_and_replace_strings -h +``` + +## Usage as a pre commit hook + +To use this package, you need to add it to your pre-commit configuration file (.pre-commit-config.yaml). Here's an example: + +For config mod + +``` +repos: + - repo: https://github.com/opencepk/find-and-replace + rev: v0.0.1 + hooks: + - id: find-and-replace-strings + name: find-and-replace-strings + description: Find and replace strings + entry: find-and-replace-strings + language: python + pass_filenames: true + exclude_types: + - binary + files: README.md + verbose: true + +``` + +and for direct mode + +``` +repos: + - repo: https://github.com/opencepk/find-and-replace + rev: v0.0.1 + hooks: + - id: find-and-replace-strings + name: find-and-replace-strings + description: Find and replace strings + entry: find-and-replace-strings + language: python + pass_filenames: true + exclude_types: + - binary + args: ["--find", "search_string", "--replacement", "replacement_string"] + files: README.md + verbose: true +``` + +Please note you also have a choice of +files: '.*\.md$' +or +files: . + +In this configuration, the find-and-replace hook is set to read search and replacement strings from a file (.project-properties.json by default which should be defined in the root of the project you want to use this package). You can also specify the search and replacement strings directly in the args field (which is not a suggested way). + +## Usage as a python package +python -m find_and_replace_strings --usage +or +find-and-replace-strings --usage + +shows some usage examples. +``` + python -m find_and_replace_strings --usage +Example usages: +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --verbose +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --log-level=DEBUG +python -m find_and_replace_strings --find 'old_string' --replacement 'new_string' example.txt --verbose +python -m find_and_replace_strings --find 'old_string' --replacement 'new_string' example1.txt example2.txt --verbose +python -m find_and_replace_strings --config my_config.json example.txt --dry-run --verbose +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --log-level=INFO +``` + +## Run tests + +``` +python -m unittest tests.test_main + +``` + +## How to run it using installed python package + +``` + pip install find-and-replace-strings + find-and-replace --config .find-and-replace.json README1.md README2.md +``` + +also if you prefer to use a direct mod + +``` +find-and-replace-strings --find "old_string" --replacement "new_string" README1.md README2.md +``` + +## Dry run + +Inside the project + +python -m find_and_replace_strings --config ./.find-and-replace.json ./README.md READMEtest.md --dry-run + +or using the deployed package + +find-and-replace-strings --config ./.find-and-replace.json ./README.md READ +MEtest.md --dry-run + +More example: + + +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --verbose + +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --log-level=DEBUG + +``` +python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --verbose +INFO:root:Running in default config file mode +INFO:root:Replacing {{BUSINESS_UNIT}} with examp"lebu in e2e/precommit-e2e.test +INFO:root:Replacing {{PROJECT_NAME}} with exampleproject in e2e/precommit-e2e.test +INFO:root:{{PROJECT_NAME}} would be replaced with exampleproject in e2e/precommit-e2e.test +INFO:root:Replacing {{GITHUB_REPO_URL}} with https://github.com/examplebu/exampleproject in e2e/precommit-e2e.test +INFO:root:Replacing {{PROJECT_DESCRIPTION_SLUG}} with Example project used to demonstrate all aspects of a project development and deployment in e2e/precommit-e2e.test +INFO:root:Replacing tucowsinc/iaascloudenablement with tucowsinc/example-github-team-name in e2e/precommit-e2e.test +INFO:root:Replacing {{PROJECT_CONTRIBUTORS}} with * [Andre Ouellet](mailto:aouellet@tucowsinc.com) in e2e/precommit-e2e.test +``` + +## If you need more help with the available flags + +``` +python -m find_and_replace_strings -h +usage: __main__.py [-h] [--config CONFIG] [--find] [--replacement] [--dry-run] + [--log-level LOG_LEVEL] [--verbose] [--usage] + [files ...] + +Perform find and replace operations on one or more target files. By default, the script reads the +search and replacement entries (strings) from a JSON file. You can also specify the search and +replacement strings directly as command line args by setting the --find "search_string" and +--replacement "replacement_string" argument options. + +positional arguments: + files File(s) on which to perform search and replace + +options: + -h, --help show this help message and exit + --config CONFIG PATH to JSON config file containing find and replacement entries + --find String to find in files + --replacement String to replace with in files + --dry-run Perform a dry run without making any changes + --log-level LOG_LEVEL + Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + --verbose Print debug and info messages + --usage Print example usages + +``` + +## Building and Publishing + +To build and publish it to pypi run with proper token + +``` +bash assets/publish.sh +``` +or create a PR and after merge the changes will be published to the artifactory. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the terms of the MIT license. + + +## Reference Info + +- https://www.gnu.org/prep/standards/html_node/Option-Table.html#Option-Table +- https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +- https://packaging.python.org/guides/distributing-packages-using-setuptools/ +- https://autopilot-docs.readthedocs.io/en/latest/license_list.html +- https://pypi.org/classifiers/ diff --git a/assets/publish.sh b/assets/publish.sh new file mode 100644 index 0000000..5133bd2 --- /dev/null +++ b/assets/publish.sh @@ -0,0 +1,18 @@ +#!/bin/bash +rm -r dist +# Check if twine and setuptools are installed +if ! python3 -c "import twine" &> /dev/null; then + echo "twine not found, installing..." + pip install twine +fi + +if ! python3 -c "import setuptools" &> /dev/null; then + echo "setuptools not found, installing..." + pip install setuptools +fi + +# Build the package +python3 setup.py sdist + +# Upload the package to PyPI +twine upload --repository-url https://upload.pypi.org/legacy/ -u __token__ -p $PYPI_TOKEN dist/* diff --git a/assets/pypi-package.png b/assets/pypi-package.png new file mode 100644 index 0000000..7e97d25 Binary files /dev/null and b/assets/pypi-package.png differ diff --git a/find_and_replace_strings/__init__.py b/find_and_replace_strings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/find_and_replace_strings/__main__.py b/find_and_replace_strings/__main__.py new file mode 100644 index 0000000..97aede6 --- /dev/null +++ b/find_and_replace_strings/__main__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from .main import main + +if __name__ == "__main__": + main() diff --git a/find_and_replace_strings/main.py b/find_and_replace_strings/main.py new file mode 100755 index 0000000..45f8487 --- /dev/null +++ b/find_and_replace_strings/main.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import fileinput +import json +import sys +import logging + + +def replace_in_file(filename, search, replacement, dry_run=False): + logging.info(f"Replacing {search} with {replacement} in {filename}") + with fileinput.FileInput(filename, inplace=not dry_run) as file: + for line in file: + if search in line and dry_run: + logging.info(f"{search} would be replaced with {replacement} in {filename}") + elif not dry_run: + print(line.replace(rf"{search}", rf"{replacement}"), end='') + + +def print_usage(): + print("Example usages:") + print("python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --verbose") + print("python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --log-level=DEBUG") + print("python -m find_and_replace_strings --find 'old_string' --replacement 'new_string' example.txt --verbose") + print("python -m find_and_replace_strings --find 'old_string' --replacement 'new_string' example1.txt example2.txt --verbose") + print("python -m find_and_replace_strings --config my_config.json example.txt --dry-run --verbose") + print("python -m find_and_replace_strings --config e2e/.find-and-replace.json e2e/precommit-e2e.test --dry-run --log-level=INFO") + + +def main(): + parser = argparse.ArgumentParser( + description="""Perform find and replace operations on one or more target files. + By default, the script reads the search and replacement entries (strings) from a JSON file. + You can also specify the search and replacement strings directly as command line args by setting the + --find "search_string" and --replacement "replacement_string" argument options.""" + ) + parser.add_argument( + '--config', default='.find-and-replace.json', + help='PATH to JSON config file containing find and replacement entries' + ) + parser.add_argument( + '--find', dest='direct_mode', action='store_true', help='String to find in files' + ) + parser.add_argument( + '--replacement', dest='direct_mode', action='store_true', help='String to replace with in files' + ) + parser.add_argument( + 'files', nargs='*', help='File(s) on which to perform search and replace' + ) + parser.add_argument( + '--dry-run', action='store_true', help='Perform a dry run without making any changes' + ) + parser.add_argument( + '--log-level', default='WARNING', help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)' + ) + parser.add_argument( + '--verbose', action='store_true', help='Print debug and info messages' + ) + parser.add_argument( + '--usage', action='store_true', help='Print example usages' + ) + args = parser.parse_args() + + if args.usage: + print_usage() + sys.exit(0) + + levels = {'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL} + level = levels.get(args.log_level.upper(), logging.WARNING) + if args.verbose: + level = logging.DEBUG + logging.basicConfig(level=level) + + if args.direct_mode: + logging.info("Running in direct mode") + for filename in args.files: + replace_in_file(filename, args.find, args.replacement, args.dry_run) + else: + logging.info("Running in default config file mode") + try: + with open(os.path.join(os.getcwd(), args.config), 'r') as f: + replacements = json.load(f) + except FileNotFoundError: + logging.error(f"Error: {args.config} file not found.") + sys.exit(1) + except json.JSONDecodeError: + logging.error(f"Error: {args.config} is not a valid JSON file.") + sys.exit(1) + + for filename in args.files: + for replacement in replacements: + replace_in_file(filename, replacement['search'], replacement['replacement'], args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/pypi_bumpversion_check/check_version.py b/pypi_bumpversion_check/check_version.py new file mode 100644 index 0000000..4392fd3 --- /dev/null +++ b/pypi_bumpversion_check/check_version.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import toml +import sys +import requests +import subprocess + + +def main(): + # Load the pyproject.toml file + data = toml.load(open("pyproject.toml")) + + # Get the current version + current_version = data["project"]["version"] + + # Check if the version is already published + response = requests.get(f"https://pypi.org/pypi/find-and-replace-strings/{current_version}/json") + + if response.status_code == 200: + print("This version is already published. Please bump the version in pyproject.toml.") + sys.exit(1) + + # Check if pyproject.toml has been modified but not committed + modified_files = subprocess.check_output(["git", "diff", "--name-only"]).decode().splitlines() + + if "pyproject.toml" in modified_files: + print("The version in pyproject.toml has been changed but not committed. Please commit your changes.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/pypi_bumpversion_check/requirements.txt b/pypi_bumpversion_check/requirements.txt new file mode 100644 index 0000000..a026d3b --- /dev/null +++ b/pypi_bumpversion_check/requirements.txt @@ -0,0 +1,5 @@ +# Requirements for pypi_bumpversion_check +toml +requests +#toml==0.10.2 +#requests==2.26.0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d8fe3bb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["find_and_replace_strings"] + +[project] +name = "find-and-replace-strings" +version = "1.1.0" +description = "Python package and pre-commit-hook for finding and replacing string(s) in file(s)." +readme = "README.md" +license = { text = "GPLv3" } +authors = [{name = "OpenCEPK Open Cloud Engineering Platform Kit", email = "opencepk@gmail.com"}] +requires-python = ">=3.9" + +keywords = ["find", "replace", "string", "file", "pre-commit", "hook", "git", "tool", "utility", "opencepk"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Topic :: File Formats :: JSON", + "Topic :: Software Development :: Pre-processors", + "Topic :: Software Development :: Version Control :: Git", + "Topic :: Text Processing", + "Topic :: Text Processing :: Filters", + "Topic :: Text Processing :: General", + "Topic :: Utilities" +] + +[project.scripts] +find-and-replace-strings = "find_and_replace_strings.__main__:main" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..977789b --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from setuptools import setup, find_packages + +setup() diff --git a/tests-package-e2e/.find-and-replace.json b/tests-package-e2e/.find-and-replace.json new file mode 100644 index 0000000..1adb550 --- /dev/null +++ b/tests-package-e2e/.find-and-replace.json @@ -0,0 +1,30 @@ +[ + { + "search": "{{BUSINESS_UNIT}}", + "replacement": "opencepk" + }, + { + "search": "{{PROJECT_NAME}}", + "replacement": "exampleproject" + }, + { + "search": "{{GITHUB_REPO_URL}}", + "replacement": "https://github.com/opencepk/opencepk-exampleproject" + }, + { + "search": "{{PROJECT_DESCRIPTION_SLUG}}", + "replacement": "Example project used to demonstrate all aspects of a project development and deployment" + }, + { + "search": "{{PROJECT_TEAM}}", + "replacement": "opencepk/opencepk-exampleteam" + }, + { + "search": "{{PROJECT_CONTRIBUTORS}}", + "replacement": "* [Open CEPK](mailto:opencepk@gmail.com)" + }, + { + "search": "{{SPECIAL_CHARACTER_TESTS}}", + "replacement": "\"SomeValueInDoubleQuotes\"" + } +] diff --git a/tests-package-e2e/README_TEST_PACKAGE.md b/tests-package-e2e/README_TEST_PACKAGE.md new file mode 100644 index 0000000..b0b9c9f --- /dev/null +++ b/tests-package-e2e/README_TEST_PACKAGE.md @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings python package + +business unit is: {{BUSINESS_UNIT}} +project name is: {{PROJECT_NAME}} +github url is: {{GITHUB_REPO_URL}} +project description slug is: {{PROJECT_DESCRIPTION_SLUG}} +project team is: {{PROJECT_TEAM}} +project contributors are: {{PROJECT_CONTRIBUTORS}} +special character tests: {{SPECIAL_CHARACTER_TESTS}} diff --git a/tests-package-e2e/README_TEST_PACKAGE.md.expected b/tests-package-e2e/README_TEST_PACKAGE.md.expected new file mode 100644 index 0000000..ac6d9f7 --- /dev/null +++ b/tests-package-e2e/README_TEST_PACKAGE.md.expected @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings python package + +business unit is: opencepk +project name is: exampleproject +github url is: https://github.com/opencepk/opencepk-exampleproject +project description slug is: Example project used to demonstrate all aspects of a project development and deployment +project team is: opencepk/opencepk-exampleteam +project contributors are: * [Open CEPK](mailto:opencepk@gmail.com) +special character tests: "SomeValueInDoubleQuotes" diff --git a/tests-package-e2e/README_TEST_PACKAGE.md.template b/tests-package-e2e/README_TEST_PACKAGE.md.template new file mode 100644 index 0000000..b0b9c9f --- /dev/null +++ b/tests-package-e2e/README_TEST_PACKAGE.md.template @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings python package + +business unit is: {{BUSINESS_UNIT}} +project name is: {{PROJECT_NAME}} +github url is: {{GITHUB_REPO_URL}} +project description slug is: {{PROJECT_DESCRIPTION_SLUG}} +project team is: {{PROJECT_TEAM}} +project contributors are: {{PROJECT_CONTRIBUTORS}} +special character tests: {{SPECIAL_CHARACTER_TESTS}} diff --git a/tests-package-e2e/check-commit-hook.sh b/tests-package-e2e/check-commit-hook.sh new file mode 100755 index 0000000..053847b --- /dev/null +++ b/tests-package-e2e/check-commit-hook.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Create target test file +cp -f "tests-e2e/target-find-and-replace.txt.template" "tests-e2e/target-find-and-replace.txt" + +# Store the original content of the file +original_content=$(cat tests-e2e/precommit-e2e.test) +echo "Original content: $original_content" + +# Run the hook +python find_and_replace_strings/main.py --config tests-e2e/.find-and-replace.json tests-e2e/precommit-e2e.test + +# Check if the expected changes have been made +content=$(cat tests-e2e/precommit-e2e.test) +echo "Content after running the hook: $content" + +if [[ "$content" != "# exampleproject" ]]; then + # If the changes are not as expected, print the exit code and exit with a status code of 1 + echo "Exit code: 1" + exit 1 +fi + +# Restore the original content of the file +echo "$original_content" > tests-e2e/precommit-e2e.test + +# If the changes are as expected, print the exit code and exit with a status code of 0 +echo "Exit code: 0" +exit 0 diff --git a/tests-package-e2e/test-package-e2e.sh b/tests-package-e2e/test-package-e2e.sh new file mode 100755 index 0000000..8deaebd --- /dev/null +++ b/tests-package-e2e/test-package-e2e.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +TEST_DIR="tests-package-e2e" +TEST_SCRIPT_FILENAME=$(basename -- "$0") +TEST_SCRIPT_NAME="${TEST_SCRIPT_FILENAME%.*}" +TEST_TARGET_FILE="README_TEST_PACKAGE.md" + +#------------------------------- +# Run the python package +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Running pre-commit using ${TEST_DIR}/.pre-commit-config.yaml" +(cd .. && python find_and_replace_strings/main.py --config ${TEST_DIR}/.find-and-replace.json ${TEST_DIR}/${TEST_TARGET_FILE}) + +#------------------------------- +# Evaluate results +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Running (diff ${TEST_TARGET_FILE} ${TEST_TARGET_FILE}.expected)" +echo "${TEST_SCRIPT_NAME}: Fetching return code of diff" +diff "${TEST_TARGET_FILE}" "${TEST_TARGET_FILE}.expected" +evaluate_diff_status=$? + +#------------------------------- +# Run the pre-commit hook reset +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Resetting ${TEST_TARGET_FILE} file to original state" +cp -pf "${TEST_TARGET_FILE}.template" "${TEST_TARGET_FILE}" + +#------------------------------- +# Exit with exit code of diff evaluation +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Exit code = $evaluate_diff_status" +exit $evaluate_diff_status diff --git a/tests-pre-commit-hook/.find-and-replace.json b/tests-pre-commit-hook/.find-and-replace.json new file mode 100644 index 0000000..1adb550 --- /dev/null +++ b/tests-pre-commit-hook/.find-and-replace.json @@ -0,0 +1,30 @@ +[ + { + "search": "{{BUSINESS_UNIT}}", + "replacement": "opencepk" + }, + { + "search": "{{PROJECT_NAME}}", + "replacement": "exampleproject" + }, + { + "search": "{{GITHUB_REPO_URL}}", + "replacement": "https://github.com/opencepk/opencepk-exampleproject" + }, + { + "search": "{{PROJECT_DESCRIPTION_SLUG}}", + "replacement": "Example project used to demonstrate all aspects of a project development and deployment" + }, + { + "search": "{{PROJECT_TEAM}}", + "replacement": "opencepk/opencepk-exampleteam" + }, + { + "search": "{{PROJECT_CONTRIBUTORS}}", + "replacement": "* [Open CEPK](mailto:opencepk@gmail.com)" + }, + { + "search": "{{SPECIAL_CHARACTER_TESTS}}", + "replacement": "\"SomeValueInDoubleQuotes\"" + } +] diff --git a/tests-pre-commit-hook/.pre-commit-config.yaml b/tests-pre-commit-hook/.pre-commit-config.yaml new file mode 100644 index 0000000..f276adb --- /dev/null +++ b/tests-pre-commit-hook/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +--- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: local + hooks: + - id: find-and-replace-strings + name: find-and-replace-strings + entry: "./find_and_replace_strings/main.py" + language: python + exclude_types: + - binary + args: ["--config", "./tests-pre-commit-hook/.find-and-replace.json"] + files: "tests-pre-commit-hook/README_TEST_PRE_COMMIT.md" diff --git a/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md new file mode 100644 index 0000000..8757397 --- /dev/null +++ b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings pre-commit hook + +business unit is: {{BUSINESS_UNIT}} +project name is: {{PROJECT_NAME}} +github url is: {{GITHUB_REPO_URL}} +project description slug is: {{PROJECT_DESCRIPTION_SLUG}} +project team is: {{PROJECT_TEAM}} +project contributors are: {{PROJECT_CONTRIBUTORS}} +special character tests: {{SPECIAL_CHARACTER_TESTS}} diff --git a/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.expected b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.expected new file mode 100644 index 0000000..f5752d5 --- /dev/null +++ b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.expected @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings pre-commit hook + +business unit is: opencepk +project name is: exampleproject +github url is: https://github.com/opencepk/opencepk-exampleproject +project description slug is: Example project used to demonstrate all aspects of a project development and deployment +project team is: opencepk/opencepk-exampleteam +project contributors are: * [Open CEPK](mailto:opencepk@gmail.com) +special character tests: "SomeValueInDoubleQuotes" diff --git a/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.template b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.template new file mode 100644 index 0000000..8757397 --- /dev/null +++ b/tests-pre-commit-hook/README_TEST_PRE_COMMIT.md.template @@ -0,0 +1,9 @@ +# Target test file for find-and-replace-strings pre-commit hook + +business unit is: {{BUSINESS_UNIT}} +project name is: {{PROJECT_NAME}} +github url is: {{GITHUB_REPO_URL}} +project description slug is: {{PROJECT_DESCRIPTION_SLUG}} +project team is: {{PROJECT_TEAM}} +project contributors are: {{PROJECT_CONTRIBUTORS}} +special character tests: {{SPECIAL_CHARACTER_TESTS}} diff --git a/tests-pre-commit-hook/test-pre-commit-hook.sh b/tests-pre-commit-hook/test-pre-commit-hook.sh new file mode 100755 index 0000000..3083a47 --- /dev/null +++ b/tests-pre-commit-hook/test-pre-commit-hook.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +TEST_DIR="tests-pre-commit-hook" +TEST_SCRIPT_FILENAME=$(basename -- "$0") +TEST_SCRIPT_NAME="${TEST_SCRIPT_FILENAME%.*}" +TEST_TARGET_FILE="README_TEST_PRE_COMMIT.md" + +#------------------------------- +# Run the pre-commit hook test +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Running pre-commit using ${TEST_DIR}/.pre-commit-config.yaml" +(cd .. && pre-commit run -a -c "${TEST_DIR}/.pre-commit-config.yaml") + +#------------------------------- +# Evaluate results +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Running (cd ${TEST_DIR} && diff ${TEST_TARGET_FILE} ${TEST_TARGET_FILE}.expected)" +echo "${TEST_SCRIPT_NAME}: Fetching return code of diff" +diff "./${TEST_TARGET_FILE}" "${TEST_TARGET_FILE}.expected" +evaluate_diff_status=$? + +#------------------------------- +# Run the pre-commit hook reset +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Resetting ${TEST_DIR}/${TEST_TARGET_FILE} file to original state" +cp -pf "${TEST_TARGET_FILE}.template" "${TEST_TARGET_FILE}" + +#------------------------------- +# Exit with exit code of diff evaluation +#------------------------------- +echo "${TEST_SCRIPT_NAME}: Exit code = $evaluate_diff_status" +exit $evaluate_diff_status diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..2241392 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import unittest +import argparse +from unittest.mock import patch, mock_open +from find_and_replace_strings.main import replace_in_file, main + + +class TestMainFunctions(unittest.TestCase): + @patch('fileinput.FileInput') + def test_replace_in_file(self, mock_fileinput): + """ + This test checks if the replace_in_file function correctly opens the file and replaces the specified text. + """ + # Mock the file input to return a specific line of text + mock_fileinput.return_value.__enter__.return_value = ['hello world'] + # Call the function with a specific search and replacement + replace_in_file('dummy.txt', 'hello', 'hi') + # Assert that the file was opened correctly + mock_fileinput.assert_called_once_with('dummy.txt', inplace=True) + + +@patch('argparse.ArgumentParser.parse_args') +@patch('find_and_replace.main.replace_in_file') +@patch('os.getcwd', return_value='/dummy/path') +@patch('builtins.open', new_callable=mock_open, read_data='{"search": "hello", "replacement": "hi"}') +@patch('json.load', return_value=[{"search": "hello", "replacement": "hi"}]) +def test_main(self, mock_json_load, mock_open, mock_getcwd, mock_replace_in_file, mock_parse_args): + """ + This test checks if the main function correctly reads the configuration file and calls the replace_in_file function with the correct arguments. + """ + # Mock the command line arguments + mock_parse_args.return_value = argparse.Namespace(files=['dummy.txt'], find=None, replacement=None, direct_mode=False, config='.find-and-replace.json') + # Call the main function + main() + # Assert that the config file was opened correctly and the replace_in_file function was called with the correct arguments + mock_open.assert_called_once_with('/dummy/path/.find-and-replace.json', 'r') + mock_replace_in_file.assert_called_once_with('dummy.txt', 'hello', 'hi') + + +if __name__ == '__main__': + unittest.main()