Repository: mrachuta/pyresizer
Pull request #3: Add Github workflows and improve quality
Author: login: mrachuta
Labels: name: minor
Created: 2025-02-27T21:14:18Z | Merged: 2025-03-03T19:00:33Z

Changes:
* add GH workflows for CI, CD and Release
* add unitt tests
* add e2e tests
* format code
* update logic responsible for resizing images
* improve error handling
* improve quality of code (including additional tools)


Diff:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 92c3c03..17e9dd4 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -1,14 +1,181 @@
 name: CD workflow
 run-name: CD workflow triggered by ${{ github.event_name }} to ${{ github.ref_name }}
-on: 
-  - workflow_dispatch
+on:
+  workflow_call:
+  workflow_dispatch:
+  pull_request:
+    types:
+      - assigned
+      - opened
+      - synchronize
+      - reopened
 
 jobs:
-  example:
-    name: Example
-    runs-on: "ubuntu-latest"
+  ci:
+    name: Call CI workflow
+    uses: ./.github/workflows/ci.yml
+
+  build-bins:
+    name: Build binaries
+    strategy:
+      matrix:
+        # ubuntu-22.04 and windows-2019 are used to ensure better compatibility with end-user os
+        os: [ubuntu-22.04, windows-2019]
+    runs-on: ${{ matrix.os }}
+    needs:
+      - ci
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
         with:
           fetch-depth: 0
+
+      - name: Initialize python
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ vars.PYTHON_VERSION }}
+          cache: "pip"
+
+      - name: Install packages
+        run: pip install -r requirements.txt
+
+      - name: Build binary
+        run: pyinstaller -F pyresizer.py
+
+      - name: Upload artifact
+        id: artifact-upload-step
+        uses: actions/upload-artifact@v4
+        with:
+          name: pyresizer-binary-${{ runner.os }}
+          path: dist
+          retention-days: 2
+          overwrite: true
+
+  e2e-tests:
+    name: Run e2e tests
+    strategy:
+      matrix:
+        os: [ubuntu-22.04, windows-2019]
+    runs-on: ${{ matrix.os }}
+    needs: build-bins
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Initialize python
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ vars.PYTHON_VERSION }}
+          cache: "pip"
+
+      - name: Install packages
+        run: pip install -r requirements.txt
+
+      - name: Download artifact
+        uses: actions/download-artifact@v4
+        with:
+          path: dist
+          name: pyresizer-binary-${{ runner.os }}
+
+      - name: Check binary (Linux)
+        if: runner.os == 'Linux'
+        run: chmod +x ./dist/pyresizer && ./dist/pyresizer -h
+
+      - name: Check binary (Windows)
+        if: runner.os == 'Windows'
+        run: ./dist/pyresizer.exe -h
+
+      - name: Run E2E tests
+        run: pytest tests/e2etests.py
+
+  validate-pr:
+    name: Validate pull request
+    runs-on: "ubuntu-22.04"
+    # Skip for release builds
+    if: github.event_name == 'pull_request'
+    needs:
+      - ci
+      - build-bins
+      - e2e-tests
+    steps:
+      - name: Check pull request
+        uses: actions/github-script@v7
+        id: checkpr
+        with:
+          script: |
+            const name = '${{ github.workflow }}';
+            const url = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}';
+
+            const targetBranch = context.payload.pull_request.base.ref;
+            const labels = context.payload.pull_request.labels.map(label => label.name.toLowerCase()) || [];
+            const validLabels = ['major', 'minor', 'patch'];
+            const matchingLabels = labels.filter(label => validLabels.includes(label));
+            
+            let labelCheckResult = null;
+            let labelCheckMessage = null;
+
+            let jobsCheckMessage = null;
+            let jobsCheckResult = null;
+
+            if (targetBranch !== 'master') {
+              labelCheckResult = 'success';
+              labelCheckMessage = 'PR label check not required, target branch is not master ✅';
+            } else if (matchingLabels.length === 1) {
+              labelCheckResult = 'success';
+              labelCheckMessage = `PR contains exactly one valid label: ${matchingLabels[0]} ✅`;
+            } else if (matchingLabels.length === 0) {
+              labelCheckResult = 'failure';
+              labelCheckMessage = 'PR must contain exactly one of the required labels: (${validLabels.join(', ')}). ❌';
+            } else {
+              labelCheckResult = 'failure';
+              labelCheckMessage = `PR contains multiple valid labels: (${matchingLabels.join(', ')}). Only one is allowed. ❌`;
+            }
+            console.log(`${labelCheckMessage}`);
+
+            const ciResult = '${{ needs.ci.result }}';
+            const buildBinsResult = '${{ needs.build-bins.result }}';
+            const e2eTestsResult = '${{ needs.e2e-tests.result }}';
+
+            const failedJobs = [];
+
+            if (ciResult !== 'success') failedJobs.push('CI');
+            if (buildBinsResult !== 'success') failedJobs.push('Build Binaries');
+            if (e2eTestsResult !== 'success') failedJobs.push('E2E Tests');
+
+            if (failedJobs.length > 0) {
+              jobsCheckMessage = `The following jobs (or job) failed: ${failedJobs.join(', ')} ❌`;
+              jobsCheckResult = 'failure';
+            } else {
+              jobsCheckMessage = 'All required jobs finished with status success ✅';
+              jobsCheckResult = 'success';
+            }
+            console.log(`${jobsCheckMessage}`);
+
+            const validationResult = (
+              `${jobsCheckResult}` === 'success' && 
+              `${labelCheckResult}` === 'success'
+            ) ? 'success' : 'failure';
+
+            const body = `${name}: ${validationResult === 'success' ? 'succeeded ✅' : 'failed ❌'}\nURL: ${url}\n- ${labelCheckMessage}\n- ${jobsCheckMessage}`;
+
+            github.rest.issues.createComment({
+              issue_number: context.issue.number,
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              body: body
+            });
+
+            core.setOutput('prresult', validationResult);
+
+            // Do not fail build in case of troubles - set commit status in next step
+
+      - name: Set pull request's latest commit status as ${{ steps.checkpr.outputs.prresult }}
+        uses: myrotvorets/set-commit-status-action@master
+        if: github.event_name == 'pull_request'
+        with:
+          sha: ${{ needs.checkout.outputs.head_sha }}
+          token: ${{ secrets.GITHUB_TOKEN }}
+          status: ${{ steps.checkpr.outputs.prresult }}
+      
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index af42684..1bd66f3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,14 +1,39 @@
-name: CI build
-run-name: CI job triggered by ${{ github.event_name }} to ${{ github.ref_name }}
-on: [workflow_dispatch]
+name: CI workflow
+run-name: CI workflow triggered by ${{ github.event_name }} to ${{ github.ref_name }}
+on: 
+  workflow_call:
+  workflow_dispatch:
+  push:
 
-jobs:
-  os_matrix:
-    strategy:
-      matrix:
-        os: [ubuntu-latest, windows-latest]
+# variable PYTHON_VERSION have to be defined at repository level
 
-    runs-on: ${{ matrix.os }}
+jobs:
+  unit-tests:
+    name: Run unit tests
+    runs-on: ubuntu-22.04
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Initialize python
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ vars.PYTHON_VERSION }}
+          # Wrapper for actions/cache@v4
+          cache: 'pip'
+
+      - name: Install packages
+        run: pip install -r requirements.txt
+
+      - name: Run unit tests and code coverage check
+        run: pytest --cov-report term --cov-report xml --cov=pyresizer tests/unittests.py
+
+  quality-checks:
+    name: Call quality checks workflow
+    uses: ./.github/workflows/quality-checks.yml
+
+  security-scans:
+    name: Call security scans workflow
+    uses: ./.github/workflows/security-scans.yml
diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml
new file mode 100644
index 0000000..a0216ad
--- /dev/null
+++ b/.github/workflows/quality-checks.yml
@@ -0,0 +1,35 @@
+name: Quality checks workflow
+
+on:
+  workflow_call:
+
+# variable PYTHON_VERSION have to be defined at repository level
+
+jobs:
+  quality-checks:
+    name: Run quality checks
+    runs-on: ubuntu-22.04
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Initialize python
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ vars.PYTHON_VERSION }}
+          cache: pip
+
+      - name: Install packages
+        run: pip install -r requirements.txt
+
+      - name: Run linters
+        run: |
+          #!/usr/bin/env bash
+          set -xeu -o pipefail
+
+          mkdir -p reports/{mypy,isort,pylint}
+          mypy . --config-file pyproject.toml
+          isort . --check-only | tee -a reports/isort/isort.log
+          pylint . --rcfile=pyproject.toml | tee -a reports/pylint/pylint.log
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 622226b..eabc033 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,11 +7,145 @@ on:
       - 'master'
 
 jobs:
-  example:
-    name: Example
-    runs-on: "ubuntu-latest"
+  cd:
+    name: Call CD workflow
+    uses: ./.github/workflows/cd.yml
+
+  release:
+    name: Create release
+    runs-on: ubuntu-22.04
+    needs: cd
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
         with:
           fetch-depth: 0
+
+      - name: Download artifact - Linux
+        uses: actions/download-artifact@v4
+        with:
+          path: binaries
+
+      - name: List artifacts
+        run: ls -la binaries
+
+      - name: Get latest PR id
+        id: latestpr
+        run: |
+          #!/usr/bin/env bash
+          set -xeu -o pipefail
+
+          LATEST_PR_ID=$(git log --grep="Merge pull request #[0-9]\+" --pretty=oneline -1 | sed -En "s/.*#([[:digit:]]+).*/\1/p";)
+          if [[ -z $LATEST_PR_ID ]]; then
+            echo "ERROR: Unable to find latest PR id!"
+            exit 101
+          fi
+          echo "Saving LATEST_PR_ID to Github variables..."
+          echo "latest_pr_id=${LATEST_PR_ID}" >> "$GITHUB_OUTPUT"
+
+      - name: Get release type
+        uses: actions/github-script@v7
+        id: releasetype
+        with:
+          script: |
+            const prId = ${{ steps.latestpr.outputs.latest_pr_id }};
+            const { owner, repo } = context.repo;
+
+            const { data: pr } = await github.rest.issues.get({
+              owner,
+              repo,
+              issue_number: parseInt(prId, 10)
+            });
+
+            const validLabels = ["major", "minor", "patch"];
+            const labels = pr.labels.map(label => label.name.toLowerCase()) || [];
+
+            const matchingLabels = labels.filter(label => validLabels.includes(label));
+
+            let releaseTypeCheckResult = null;
+            let releaseTypeCheckMessage = null;
+
+            if (matchingLabels.length === 0) {
+              releaseTypeCheckResult = 'failure';
+              releaseTypeCheckMessage = `PR must contain exactly one of the required labels: (${validLabels.join(', ')}). ❌`;
+            } else if (matchingLabels.length > 1) {
+              releaseTypeCheckResult = 'failure';
+              releaseTypeCheckMessage = `PR contains multiple valid labels: (${matchingLabels.join(', ')}). Only one is allowed. ❌`;
+            } else {
+              releaseTypeCheckResult = 'success';
+              releaseTypeCheckMessage = `PR contains exactly one valid label: ${matchingLabels[0]} ✅`;
+            }
+
+            console.log(`${releaseTypeCheckMessage}`);
+            if (`${releaseTypeCheckResult}` !== 'success') core.setFailed(`${releaseTypeCheckMessage}`);
+            core.setOutput("pr_label", matchingLabels[0]);
+
+      - name: Calculate new tag
+        id: calculatetag
+        run: |
+          #!/usr/bin/env bash
+          set -xeu -o pipefail
+
+          # Convert to lower case
+          RELEASE_TYPE=$(echo ${{ steps.releasetype.outputs.pr_label }} | tr '[:upper:]' '[:lower:]')
+          PREFIXED_TAG=false
+          if [[ -z $RELEASE_TYPE ]]; then
+            echo "ERROR: Unable to determine release type!"
+            exit 101
+          fi
+
+          mapfile CURRENT_TAGS < <(git tag --sort=-v:refname)
+
+          if [[ ${#CURRENT_TAGS[@]} == 0 ]]; then
+            echo "No tags found, creating initial tag: v1.0.0"
+            NEW_TAG='v1.0.0'
+          else
+            echo "Following tags were found: " "${CURRENT_TAGS[@]}"
+            echo "Latest tag is: ${CURRENT_TAGS[0]}"
+            latest_tag=${CURRENT_TAGS[0]}
+            if [[ $latest_tag == v* ]]; then
+              echo "Tag prefixed with 'v', removing prefix temporary"
+              latest_tag=${CURRENT_TAGS[0]#v}
+              PREFIXED_TAG=true
+            fi
+            IFS='.' read -r major minor patch <<< "$latest_tag"
+            next_major_version="$((major + 1)).0.0"
+            next_minor_version="${major}.$((minor + 1)).0"
+            next_patch_version="${major}.${minor}.$((patch + 1))"
+            case "$RELEASE_TYPE" in
+              "major") NEW_TAG=$next_major_version ;;
+              "minor") NEW_TAG=$next_minor_version ;;
+              "patch") NEW_TAG=$next_patch_version ;;
+              *) echo "Unknow release type ${RELEASE_TYPE}" && exit 102
+            esac
+          fi
+          if [[ "$PREFIXED_TAG" == true ]]; then
+              NEW_TAG="v${NEW_TAG}"
+          fi
+          echo "New tag to be created: ${NEW_TAG}"
+          echo "Saving NEW_TAG to Github variables..."
+          echo "new_tag=${NEW_TAG}" >> "$GITHUB_OUTPUT"
+
+      - name: Create packages
+        run: |
+          #!/usr/bin/env bash
+          set -xeu -o pipefail
+
+          NEW_TAG=${{ steps.calculatetag.outputs.new_tag }}
+          ROOT_DIR=$PWD
+          cp README.md binaries/pyresizer-binary-Linux/
+          cp README.md binaries/pyresizer-binary-Windows/
+          echo "Create Linux release..."
+          cd "${ROOT_DIR}/binaries/pyresizer-binary-Linux/" && tar -cvf "pyresizer-${NEW_TAG}-linux-x64.tar" ./* && mv ./*.tar "${ROOT_DIR}/"
+          echo "Create Windows release..."
+          cd "${ROOT_DIR}/binaries/pyresizer-binary-Windows/" && zip -vr "pyresizer-${NEW_TAG}-windows-x64.zip" ./* && mv ./*.zip "${ROOT_DIR}/"
+
+      - name: Create Github release
+        uses: ncipollo/release-action@v1
+        with:
+          artifacts: "pyresizer-${{ steps.calculatetag.outputs.new_tag }}-linux-x64.tar,pyresizer-${{ steps.calculatetag.outputs.new_tag }}-windows-x64.zip"
+          body: "New release ${{ steps.calculatetag.outputs.new_tag }}"
+          generateReleaseNotes: true
+          commit: ${{ github.sha }}
+          name: ${{ steps.calculatetag.outputs.new_tag }}
+          tag: ${{ steps.calculatetag.outputs.new_tag }}
diff --git a/.github/workflows/security-scans.yml b/.github/workflows/security-scans.yml
new file mode 100644
index 0000000..7255f33
--- /dev/null
+++ b/.github/workflows/security-scans.yml
@@ -0,0 +1,28 @@
+name: Security scans workflow
+
+on:
+  workflow_call:
+
+jobs:
+  security-scans:
+    name: Run security scans
+    runs-on: ubuntu-22.04
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Run secrets scanning
+        uses: trufflesecurity/trufflehog@main
+        with:
+          extra_args: --results=verified,unknown
+
+      - name: Run SCA scan
+        if: runner.os == 'Linux'
+        uses: pypa/gh-action-pip-audit@v1.1.0
+        with:
+          inputs: requirements.txt
+
+      - name: Run SAST scan
+        uses: PyCQA/bandit-action@v1
diff --git a/.gitignore b/.gitignore
index e5bd10a..ccd4609 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 #Ignore selected project-files
 build
 dist
-bak
 *.spec
 Thumbs.db
 pyresizer-icon.ico
@@ -9,4 +8,6 @@ pyresizer-icon.ico
 __pycache__
 *.log
 *.pot
-*.pyc
\ No newline at end of file
+*.pyc
+*.coverage
+reports/**
diff --git a/README.md b/README.md
index f5e0054..248ef43 100644
--- a/README.md
+++ b/README.md
@@ -28,9 +28,10 @@ App is aimed to:
 Just Python.
 
 Code was tested on following platforms:
-  - Windows 10 with Python 3.11.0 
-  - Windows 11 with Python 3.13.0
-  - Linux with Python 3.13.1
+  - Windows 10 with Python 3.9
+  - Windows 11 with Python 3.13
+  - Linux with Python 3.13
+  - Linux with Python 3.9
 
 Libraries required to run are available in *requirements.txt* file.
 
@@ -70,7 +71,7 @@ Libraries required to run are available in *requirements.txt* file.
 1. Perform following command to remove tool:
      - Windows
        ```
-       pyresizer.exe -ui
+       pyresizer.exe -u
        ```
        This command removes pyresizer from your App Data directory and the entry from the context menu.
      - Linux
@@ -85,9 +86,9 @@ Libraries required to run are available in *requirements.txt* file.
   ```
   pyresizer
   ```
-- Run with custom image size
+- Run with custom image width (height will be adjusted automatically to keep aspect ration)
   ```
-  pyresizer -x 2400 -y 3200
+  pyresizer -x 2000
   ```
 - Help
   ```
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ee9b570
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,45 @@
+[project]
+name = "pyresizer"
+description = "Script to quickly resize images."
+requires-python = ">=3.9"
+
+[tool.pytest.ini_options]
+addopts = "-v --doctest-modules"
+
+[tool.coverage.run]
+branch = true
+source = ["pyresizer"]
+
+[tool.coverage.report]
+precision  = 2
+skip_empty = true
+fail_under = 66.00
+exclude_also = [
+    'def _remove_from_windows_context_menu',
+    'def _add_to_windows_context_menu',
+    'def _add_to_linux_path',
+    'def _remove_from_linux_path'
+]
+
+[tool.coverage.xml]
+output = "reports/coverage/coverage.xml"
+
+[tool.mypy]
+python_version = "3.9"
+cobertura_xml_report = "reports/mypy/"
+
+[tool.black]
+line-length = 88
+target-version = ['py39']
+include = '\.pyi?$'
+
+[tool.pylint.basic]
+recursive = true
+fail-under = 8
+reports = true
+output-format = "parseable"
+disable = ["C0116", "C0115", "C0114", "E0401"]
+
+[tool.isort]
+skip_gitignore = true
+profile = "black"
diff --git a/pyresizer.py b/pyresizer.py
index a1810b8..230e180 100644
--- a/pyresizer.py
+++ b/pyresizer.py
@@ -1,18 +1,19 @@
 #!/usr/bin/env python3
 
-import platform
 import argparse
+import platform
 import sys
-from re import compile as re_compile, MULTILINE as re_MULTILINE
-from shutil import copy2 as shutil_copy2, rmtree as shutil_rmtree
-from os import (
-    listdir as os_listdir,
-    path as os_path,
-    mkdir as os_mkdir,
-    remove as os_remove,
-)
-from PIL.Image import open as pil_open
+from os import listdir as os_listdir
+from os import mkdir as os_mkdir
+from os import path as os_path
+from os import remove as os_remove
+from re import MULTILINE as re_MULTILINE
+from re import compile as re_compile
+from shutil import copy2 as shutil_copy2
+from shutil import rmtree as shutil_rmtree
+
 from PIL.Image import Resampling as pil_Resampling
+from PIL.Image import open as pil_open
 
 if platform.system() == "Windows":
     import winreg
@@ -21,9 +22,9 @@
 class Resizer:
     """Main functionality of tool"""
 
-    def __init__(self, new_width, new_height):
+    def __init__(self, new_width):
         # Both sizes could be changed according to requirements
-        self.img_dims = (new_width, new_height)
+        self.new_width = new_width
         self.img_formats = [".bmp", ".gif", ".jpg", ".jpeg", ".png"]
         self.bak_folder = "bak"
 
@@ -45,13 +46,11 @@ def make_backups(self):
             for i in self.get_imgs:
                 shutil_copy2(i, os_path.join(self.bak_folder, i))
             print("Backup created.")
+            return True
         except IOError:
             print("Error: unable to create backup!")
             dec = input("Do you want to proceed to next step? [y/n]: ")
-            if dec.lower() == "y":
-                return True
-            else:
-                return False
+            return bool(dec.lower() == "y")
 
     def resize_files(self):
 
@@ -66,10 +65,14 @@ def resize_files(self):
             for index, i in enumerate(self.get_imgs):
                 print(f"Resizing {i} ({index+1} of {imgs_quantity})...")
                 im = pil_open(i)
-                im.thumbnail(self.img_dims, pil_Resampling.LANCZOS)
+                im_width, im_height = im.size
+                new_height = self.new_width * im_height / im_width
+                img_dims = (self.new_width, new_height)
+                im.thumbnail(img_dims, pil_Resampling.LANCZOS)
                 im.save(i)
                 print(f"Resizing {i} finished.")
             print("Processing finished.")
+            return True
         except IOError as exc:
             raise IOError("Error: some files were not processed!") from exc
 
@@ -77,6 +80,8 @@ def resize_files(self):
 class InstallerUninstaller:
     """Installation or uninstallation of tool"""
 
+    textEncoding = "utf-8"
+
     def __init__(self, app_name):
 
         if not getattr(sys, "frozen", False):
@@ -118,6 +123,7 @@ def _remove_from_windows_context_menu(self):
                 winreg.DeleteKey(winreg.HKEY_CURRENT_USER, f"{self.reg_path}\\command")
                 winreg.DeleteKey(winreg.HKEY_CURRENT_USER, self.reg_path)
                 print("Registry keys removed.")
+                return True
             except OSError as exc:
                 raise OSError("Error: unable to remove registry keys!") from exc
         else:
@@ -151,9 +157,9 @@ def _add_to_windows_context_menu(self):
                 winreg.CloseKey(reg_key_command)
                 winreg.CloseKey(reg_key)
                 print("Registry keys added successfully.")
-            except OSError:
-                print("Error: unable to add registry keys!")
-                return False
+                return True
+            except OSError as exc:
+                raise OSError("Error: unable to add registry keys!") from exc
         else:
             raise SystemError(
                 "Error: cannot continue installation. Unsupported platform."
@@ -185,45 +191,62 @@ def _add_to_linux_path(self):
                         existing_files.extend(files_in_dir)
                     else:
                         existing_files.append(bash_path)
-            except Exception as e:
-                print(f"Error: error during reading object: {e}")
+            except Exception as exc:
+                raise SystemError(
+                    f"Error: error during reading object: {bash_path}"
+                ) from exc
 
         for bash_file in existing_files:
             try:
                 print(
                     f"Checking file {bash_file} against presence of $HOME/.local/bin in $PATH..."
                 )
-                with open(bash_file, "r") as f:
+                with open(bash_file, "r", encoding=self.textEncoding) as f:
                     for line in f:
                         if re_compile(r"^PATH=.*\$HOME\/\.local\/bin").search(line):
                             print(
-                                f"$HOME/.local/bin found in $PATH in file {bash_file}. Changes in $PATH not needed."
+                                f"$HOME/.local/bin found in $PATH in file {bash_file}. "
+                                + "Changes in $PATH not needed."
                             )
                             return True
-            except Exception as e:
-                print(f"Error: error during reading object {bash_file}: {e}")
+            except Exception as exc:
+                raise SystemError(
+                    f"Error: error during reading object {bash_file}"
+                ) from exc
 
         print(
-            f"$HOME/.local/bin not found in $PATH in any of files. Adding changes to {self.default_bash_file}"
+            "$HOME/.local/bin not found in $PATH in any of files. "
+            + f"Adding changes to {self.default_bash_file}"
         )
         try:
-            with open(f"{self.default_bash_file}", "a") as f:
+            with open(
+                f"{self.default_bash_file}", "a", encoding=self.textEncoding
+            ) as f:
                 f.write(
-                    '# Update added by pyresizer\nPATH="$HOME/.local/bin:$PATH"\nexport $PATH\n# End of pyresizer update\n\n'
+                    "# Update added by pyresizer\n"
+                    + 'PATH="$HOME/.local/bin:$PATH"\nexport $PATH\n'
+                    + "# End of pyresizer update\n\n"
                 )
             print("$PATH modified. Reload your bash please.")
-        except Exception as e:
-            print(f"Error: error during writing object {self.default_bash_file}: {e}")
+            return True
+        except Exception as exc:
+            raise SystemError(
+                f"Error: error during writing object {self.default_bash_file}"
+            ) from exc
 
     def _remove_from_linux_path(self):
 
         try:
             print("Removing application from $PATH...")
-            with open(f"{self.default_bash_file}", "r") as f:
+            with open(
+                f"{self.default_bash_file}", "r", encoding=self.textEncoding
+            ) as f:
                 file_content = f.read()
             # Get content and replace
             update_pattern = re_compile(
-                r'# Update added by pyresizer\nPATH="\$HOME/\.local/bin:\$PATH"\nexport \$PATH\n# End of pyresizer update\n\n',
+                r"# Update added by pyresizer\n"
+                + r'PATH="\$HOME/\.local/bin:\$PATH"\nexport \$PATH\n'
+                + r"# End of pyresizer update\n\n",
                 re_MULTILINE,
             )
             if not update_pattern.search(file_content):
@@ -233,13 +256,16 @@ def _remove_from_linux_path(self):
                 return True
 
             new_content = update_pattern.sub("", file_content)
-            with open(f"{self.default_bash_file}", "w") as f:
+            with open(
+                f"{self.default_bash_file}", "w", encoding=self.textEncoding
+            ) as f:
                 f.write(new_content)
             print("$PATH modified. Reload your bash please.")
-        except Exception as e:
-            print(
-                f"Error: error during reading or writing object {self.default_bash_file}: {e}"
-            )
+            return True
+        except Exception as exc:
+            raise SystemError(
+                f"Error: error during reading or writing object {self.default_bash_file}"
+            ) from exc
 
     def copy_file(self):
 
@@ -265,6 +291,7 @@ def copy_file(self):
             self._add_to_linux_path()
 
         print("Installation completed!")
+        return True
 
     def remove_file(self):
 
@@ -302,34 +329,44 @@ def remove_file(self):
             self._remove_from_linux_path()
 
         print("Uninstallation completed!")
+        return True
 
 
 def main():
 
+    sys.stdout.reconfigure(encoding=InstallerUninstaller.textEncoding)
     script = os_path.basename(sys.argv[0])
     app_name = "pyresizer"
-    desc = f"{app_name} 2.0.0. Script to quickly resize images."
+    desc = f"{app_name} 2.1.0. Script to quickly resize images."
     parser = argparse.ArgumentParser(prog=script, description=desc)
 
     print(
-        """\
+        """\n
         ┌─┐┬ ┬┬─┐┌─┐┌─┐┬┌─┐┌─┐┬─┐
         ├─┘└┬┘├┬┘├┤ └─┐│┌─┘├┤ ├┬┘
         ┴   ┴ ┴└─└─┘└─┘┴└─┘└─┘┴└─
-        \n
         \nType -h or --help to see more information.
         """
     )
 
     parser.add_argument(
-        "-i", "--install", help="Install to the context menu", action="store_true"
+        "-i",
+        "--install",
+        help="Install to the context menu",
+        action="store_true",
     )
     parser.add_argument(
-        "-u", "--uninstall", help="Remove from the context menu", action="store_true"
+        "-u",
+        "--uninstall",
+        help="Remove from the context menu",
+        action="store_true",
     )
-    parser.add_argument("-x", "--width", help="New image width", type=int, default=1200)
     parser.add_argument(
-        "-y", "--height", help="New image height", type=int, default=1600
+        "-x",
+        "--width",
+        help="New image width (height will be adjusted automatically to keep aspect ration)",
+        type=int,
+        default=1920,
     )
     args = parser.parse_args()
     if args.install:
@@ -341,7 +378,7 @@ def main():
         uninstaller = InstallerUninstaller(app_name)
         uninstaller.remove_file()
     else:
-        resizer = Resizer(args.width, args.height)
+        resizer = Resizer(args.width)
         resizer.resize_files()
         input("Press ENTER key to exit...")
 
diff --git a/requirements.txt b/requirements.txt
index a5078c6..dcc1926 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,11 @@ macholib==1.16.3
 Pillow>=11.1.0
 PyInstaller>=6.11.1
 pywin32-ctypes==0.2.3
+pylint==3.3.4
+mypy==1.15.0
+isort==6.0.0
+black==25.1.0
+lxml==5.3.1
+coverage==7.6.12
+pytest==8.3.4
+pytest-cov==6.0.0
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/e2etests.py b/tests/e2etests.py
new file mode 100644
index 0000000..31372b4
--- /dev/null
+++ b/tests/e2etests.py
@@ -0,0 +1,107 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            # Explanation: https://stackoverflow.com/a/26417712
+            "shell": True if platform.system() == "Windows" else False,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable, "--help"], **self.subprocess_required_args
+        )
+        self.assertIn("Script to quickly resize images.", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 1920)
+            self.assertEqual(image_png_height, 1080)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 1920)
+            self.assertEqual(image_jpg_height, 1080)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable, "--width", "2000"], **self.subprocess_required_args
+        )
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 2000)
+            self.assertEqual(image_png_height, 1125)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 2000)
+            self.assertEqual(image_jpg_height, 1125)
+
+    def testBackupsCreated(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, "bak", self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 3840)
+            self.assertEqual(image_png_height, 2160)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, "bak", self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 3840)
+            self.assertEqual(image_jpg_height, 2160)
diff --git a/tests/resources/full-black-4k.png b/tests/resources/full-black-4k.png
new file mode 100644
index 0000000..3b04ada
Binary files /dev/null and b/tests/resources/full-black-4k.png differ
diff --git a/tests/resources/full-red-4k.jpg b/tests/resources/full-red-4k.jpg
new file mode 100644
index 0000000..c8abc7f
Binary files /dev/null and b/tests/resources/full-red-4k.jpg differ
diff --git a/tests/unittests.py b/tests/unittests.py
new file mode 100644
index 0000000..1129a52
--- /dev/null
+++ b/tests/unittests.py
@@ -0,0 +1,308 @@
+import os
+import sys
+import unittest
+from unittest.mock import MagicMock, patch
+
+# Add root directory to sys.path so we can import pyresizer
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+from pyresizer import InstallerUninstaller, Resizer
+
+# Unit tests generated with support of DeepSeek AI
+
+
+class TestResizer(unittest.TestCase):
+
+    def setUp(self):
+        # Setup code that runs before each test
+        self.new_width = 640
+        self.resizer = Resizer(self.new_width)
+
+    def tearDown(self):
+        # Stop all patches after each test
+        patch.stopall()
+
+    def test_initialization(self):
+        self.assertEqual(self.resizer.new_width, self.new_width)
+        self.assertEqual(
+            self.resizer.img_formats, [".bmp", ".gif", ".jpg", ".jpeg", ".png"]
+        )
+        self.assertEqual(self.resizer.bak_folder, "bak")
+
+    def test_initialization_with_no_arguments(self):
+        with self.assertRaises(TypeError):
+            Resizer()
+
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    def test_get_imgs(self, mock_isfile, mock_listdir):
+        mock_listdir.return_value = [
+            "test.bmp",
+            "test.gif",
+            "test.jpg",
+            "test.txt",
+            "test.png",
+        ]
+        # Simulate that all files are valid files
+        mock_isfile.side_effect = lambda x: True
+
+        imgs = self.resizer.get_imgs
+
+        self.assertEqual(imgs, ["test.bmp", "test.gif", "test.jpg", "test.png"])
+
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    def test_get_imgs_no_images(self, mock_isfile, mock_listdir):
+
+        mock_listdir.return_value = ["test.txt", "test.doc", "test.pdf"]
+        mock_isfile.side_effect = lambda x: True
+
+        imgs = self.resizer.get_imgs
+
+        self.assertEqual(imgs, [])
+
+    @patch("pyresizer.os_path.exists")
+    @patch("pyresizer.os_mkdir")
+    @patch("pyresizer.shutil_copy2")
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    def test_make_backups_success(
+        self, mock_isfile, mock_listdir, mock_copy2, mock_mkdir, mock_exists
+    ):
+
+        mock_listdir.return_value = ["test.bmp", "test.gif", "test.jpg", "test.png"]
+        mock_isfile.side_effect = lambda x: True
+        mock_exists.return_value = False
+
+        result = self.resizer.make_backups()
+
+        # Assert that the backup folder was created
+        mock_mkdir.assert_called_once_with(self.resizer.bak_folder)
+        # Assert that shutil_copy2 was called for each image
+        self.assertEqual(mock_copy2.call_count, len(self.resizer.get_imgs))
+
+        self.assertTrue(result)
+
+    @patch("pyresizer.os_path.exists")
+    @patch("pyresizer.os_mkdir")
+    @patch("pyresizer.shutil_copy2")
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    @patch("builtins.input", lambda *args: "n")
+    def test_make_backups_failure(
+        self, mock_isfile, mock_listdir, mock_copy2, mock_mkdir, mock_exists
+    ):
+        mock_listdir.return_value = ["test.bmp", "test.gif", "test.jpg", "test.png"]
+        mock_isfile.side_effect = lambda x: True
+        # Simulate that the backup folder does not exist initially
+        mock_exists.return_value = False
+        # Simulate an IOError during the backup process
+        mock_copy2.side_effect = IOError("Backup failed")
+
+        result = self.resizer.make_backups()
+
+        mock_mkdir.assert_called_once_with(self.resizer.bak_folder)
+
+        self.assertFalse(result)
+
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    @patch("pyresizer.pil_open")
+    @patch("pyresizer.shutil_copy2")
+    @patch("pyresizer.os_path.exists")
+    @patch("pyresizer.os_mkdir")
+    def test_resize_files_success(
+        self,
+        mock_mkdir,
+        mock_exists,
+        mock_copy2,
+        mock_pil_open,
+        mock_isfile,
+        mock_listdir,
+    ):
+
+        mock_listdir.return_value = ["test.jpg", "test.png"]
+        mock_isfile.side_effect = lambda x: True
+
+        mock_exists.return_value = False
+        # Mock the PIL image object
+        mock_image = MagicMock()
+        mock_pil_open.return_value = mock_image
+        # Mock the Resampling.LANCZOS value
+        mock_resampling = MagicMock()
+        # Mock size
+        mock_image.size = (1600, 1200)
+        mock_resampling.LANCZOS = 1  # Simulate the actual value of Resampling.LANCZOS
+        mock_pil_open.return_value.Resampling = mock_resampling
+
+        # Call the resize_files method
+        result = self.resizer.resize_files()
+
+        mock_mkdir.assert_called_once_with(self.resizer.bak_folder)
+
+        self.assertEqual(mock_copy2.call_count, len(self.resizer.get_imgs))
+        # Assert that the image was resized and saved for each image
+        self.assertEqual(mock_image.thumbnail.call_count, len(self.resizer.get_imgs))
+        for call in mock_image.thumbnail.call_args_list:
+            expected_new_height = self.resizer.new_width * 1200 / 1600
+            self.assertEqual(
+                call,
+                unittest.mock.call(
+                    (self.resizer.new_width, expected_new_height),
+                    mock_resampling.LANCZOS,
+                ),
+            )
+
+        self.assertTrue(result)
+
+    @patch("pyresizer.os_listdir")
+    @patch("pyresizer.os_path.isfile")
+    @patch("pyresizer.pil_open")
+    @patch("pyresizer.shutil_copy2")
+    @patch("pyresizer.os_path.exists")
+    @patch("pyresizer.os_mkdir")
+    def test_resize_files_failure(
+        self,
+        mock_mkdir,
+        mock_exists,
+        mock_copy2,
+        mock_pil_open,
+        mock_isfile,
+        mock_listdir,
+    ):
+
+        mock_listdir.return_value = ["test.jpg", "test.png"]
+        mock_isfile.side_effect = lambda x: True
+        mock_exists.return_value = False
+
+        mock_pil_open.side_effect = IOError("Resizing failed")
+
+        # Call the resize_files method and expect an IOError
+        with self.assertRaises(IOError) as context:
+            self.resizer.resize_files()
+
+        mock_mkdir.assert_called_once_with(self.resizer.bak_folder)
+        # Assert that the correct error message was raised
+        self.assertEqual(
+            str(context.exception), "Error: some files were not processed!"
+        )
+
+
+class TestInstallerUninstaller(unittest.TestCase):
+
+    @patch("pyresizer.sys")
+    @patch("pyresizer.platform")
+    def setUp(self, mock_platform, mock_sys):
+
+        mock_sys.frozen = True
+        mock_platform.system.return_value = "Windows"
+        self.installeruninstaller = InstallerUninstaller("testapp")
+
+    def tearDown(self):
+
+        patch.stopall()
+
+    @patch("pyresizer.sys")
+    def test_initialization_not_frozen(self, mock_sys):
+
+        mock_sys.frozen = False
+        with self.assertRaises(SystemError) as context:
+            # Initialize object again
+            InstallerUninstaller("testapp")
+        self.assertEqual(
+            str(context.exception),
+            "Error: cannot install Python file because of dependecies.",
+        )
+
+    @patch("pyresizer.platform")
+    def test_initialization_with_no_arguments(self, mock_platform):
+
+        mock_platform.system.return_value = "Darwin"
+        # Test that creating a Resizer object with no arguments raises a TypeError
+        with self.assertRaises(TypeError):
+            InstallerUninstaller()
+
+    @patch("pyresizer.sys")
+    @patch("pyresizer.platform")
+    def test_initialization_unsupported_platform(self, mock_platform, mock_sys):
+
+        mock_sys.frozen = True
+        mock_platform.system.return_value = "Darwin"
+        with self.assertRaises(SystemError) as context:
+            InstallerUninstaller("pyresizer")
+        self.assertEqual(
+            str(context.exception),
+            "Error: cannot continue installation. Unsupported platform.",
+        )
+
+    @patch("pyresizer.platform")
+    def test_copy_file_unsupported_platform(self, mock_platform):
+
+        mock_platform.system.return_value = "Darwin"
+        # Use already initialized object
+        installer = self.installeruninstaller
+        with self.assertRaises(SystemError) as context:
+            installer.copy_file()
+        self.assertEqual(
+            str(context.exception),
+            "Error: cannot continue installation. Unsupported platform.",
+        )
+
+    @patch("pyresizer.os_path")
+    @patch("pyresizer.shutil_copy2")
+    def test_copy_file_io_error(self, mock_copy2, mock_os_path):
+
+        mock_os_path.exists.return_value = False
+        mock_copy2.side_effect = IOError("File copy failed")
+        installer = self.installeruninstaller
+        with self.assertRaises(IOError) as context:
+            installer.copy_file()
+        self.assertEqual(str(context.exception), "Error: executable file not copied!")
+
+    @patch("pyresizer.platform")
+    def test_remove_file_unsupported_platform(self, mock_platform):
+
+        mock_platform.system.return_value = "Darwin"
+        installer = self.installeruninstaller
+        with self.assertRaises(SystemError) as context:
+            installer.remove_file()
+        self.assertEqual(
+            str(context.exception),
+            "Error: cannot continue uninstallation. Unsupported platform.",
+        )
+
+    @patch("pyresizer.platform")
+    @patch("pyresizer.os_path")
+    @patch("pyresizer.shutil_rmtree")
+    def test_remove_file_os_error_windows(
+        self, mock_rmtree, mock_os_path, mock_platform
+    ):
+
+        # Mock again because of second if
+        mock_platform.system.return_value = "Windows"
+        mock_os_path.exists.return_value = True
+        mock_rmtree.side_effect = OSError("Directory removal failed")
+        installer = self.installeruninstaller
+        with self.assertRaises(OSError) as context:
+            installer.remove_file()
+        self.assertEqual(
+            str(context.exception),
+            "Error: subdirectory together with executable file not removed!",
+        )
+
+    @patch("pyresizer.platform")
+    @patch("pyresizer.os_path")
+    @patch("pyresizer.shutil_rmtree")
+    def test_remove_file_os_error_linux(self, mock_rmtree, mock_os_path, mock_platform):
+
+        mock_platform.system.return_value = "Linux"
+        mock_os_path.exists.return_value = True
+        mock_rmtree.side_effect = OSError("Directory removal failed")
+        installer = self.installeruninstaller
+        with self.assertRaises(OSError) as context:
+            installer.remove_file()
+        self.assertEqual(str(context.exception), "Error: executable file not removed!")
+
+
+if __name__ == "__main__":
+    unittest.main()


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-27T21:14:25Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T17:59:31Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T18:15:42Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T18:40:16Z

Review comments:
diff_hunk: @@ -0,0 +1,29 @@
+import os
+import platform
+import subprocess
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: Consider possible security implications associated with the subprocess module.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/4)
created_at: 2025-02-27T21:14:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,29 @@
+import os
+import platform
+import subprocess
+import sys
+import unittest
+
+
+class TestE2E(unittest.TestCase):
+
+    def setUp(self):
+        self.subprocess_required_args = {
+            "cwd": f"{os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'dist')}",
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable, "--help"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/2)
created_at: 2025-02-27T21:14:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: Consider possible security implications associated with the subprocess module.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/5)
created_at: 2025-03-03T17:59:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/6)
created_at: 2025-03-03T17:59:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/7)
created_at: 2025-03-03T17:59:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        image_png = PILImage.open(os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME))
+        image_png_width, image_png_height = image_png.size
+        image_jpg = PILImage.open(os.path.join(self.SCRIPT_PATH, self.RED_FILENAME))
+        image_jpg_width, image_jpg_height = image_jpg.size
+        self.assertEqual(image_png_width, 1200)
+        self.assertEqual(image_png_height, 675)
+        self.assertEqual(image_jpg_width, 1200)
+        self.assertEqual(image_jpg_height, 675)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable + " --width 2000"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/8)
created_at: 2025-03-03T17:59:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        image_png = PILImage.open(os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME))
+        image_png_width, image_png_height = image_png.size
+        image_jpg = PILImage.open(os.path.join(self.SCRIPT_PATH, self.RED_FILENAME))
+        image_jpg_width, image_jpg_height = image_jpg.size
+        self.assertEqual(image_png_width, 1200)
+        self.assertEqual(image_png_height, 675)
+        self.assertEqual(image_jpg_width, 1200)
+        self.assertEqual(image_jpg_height, 675)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable + " --width 2000"], **self.subprocess_required_args
+        )
+        image_png = PILImage.open(os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME))
+        image_png_width, image_png_height = image_png.size
+        image_jpg = PILImage.open(os.path.join(self.SCRIPT_PATH, self.RED_FILENAME))
+        image_jpg_width, image_jpg_height = image_jpg.size
+        self.assertEqual(image_png_width, 2000)
+        self.assertEqual(image_png_height, 1125)
+        self.assertEqual(image_jpg_width, 2000)
+        self.assertEqual(image_jpg_height, 1125)
+
+    def testBackupsCreated(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/9)
created_at: 2025-03-03T17:59:31Z
author_association: NONE
diff_hunk: @@ -0,0 +1,106 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/10)
created_at: 2025-03-03T18:15:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,106 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 1200)
+            self.assertEqual(image_png_height, 675)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 1200)
+            self.assertEqual(image_jpg_height, 675)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable + " --width 2000"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/11)
created_at: 2025-03-03T18:15:42Z
author_association: NONE
diff_hunk: @@ -0,0 +1,106 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            "shell": True,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable + " --help"], **self.subprocess_required_args
+        )
+        self.assertIn("optional arguments:", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 1200)
+            self.assertEqual(image_png_height, 675)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 1200)
+            self.assertEqual(image_jpg_height, 675)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable + " --width 2000"], **self.subprocess_required_args
+        )
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 2000)
+            self.assertEqual(image_png_height, 1125)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 2000)
+            self.assertEqual(image_jpg_height, 1125)
+
+    def testBackupsCreated(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/12)
created_at: 2025-03-03T18:15:42Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            # Explanation: https://stackoverflow.com/a/26417712
+            "shell": True if platform.system() == "Windows" else False,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable, "--help"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/13)
created_at: 2025-03-03T18:40:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+import os
+import platform
+import re
+import shutil
+import subprocess
+import time
+import unittest
+
+from PIL import Image as PILImage
+
+
+class TestE2E(unittest.TestCase):
+
+    CWD = os.path.dirname(os.path.abspath(__file__))
+    SCRIPT_PATH = os.path.join(CWD, "..", "dist")
+    BLACK_FILENAME = "full-black-4k.png"
+    RED_FILENAME = "full-red-4k.jpg"
+
+    def setUp(self):
+
+        self.subprocess_required_args = {
+            "cwd": self.SCRIPT_PATH,
+            "capture_output": True,
+            "text": True,
+            # Explanation: https://stackoverflow.com/a/26417712
+            "shell": True if platform.system() == "Windows" else False,
+            "encoding": "utf-8",
+        }
+        if platform.system() == "Windows":
+            self.executable = "pyresizer.exe"
+        elif platform.system() == "Linux":
+            self.executable = "./pyresizer"
+        else:
+            raise SystemError("Error: cannot continue E2E tests. Unsupported platform.")
+        # Copy resources required by tests
+        files_to_copy = [
+            os.path.join(self.CWD, "resources", f)
+            for f in os.listdir(os.path.join(self.CWD, "resources"))
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        for f in files_to_copy:
+            shutil.copy2(f, self.SCRIPT_PATH)
+
+    def tearDown(self):
+        files_to_remove = [
+            os.path.join(self.SCRIPT_PATH, f)
+            for f in os.listdir(self.SCRIPT_PATH)
+            if re.search(r".*\.(jpg|png)$", f)
+        ]
+        # Ignore files with new size
+        for f in files_to_remove:
+            os.remove(f)
+        # Remove bak folder and it's content
+        shutil.rmtree(os.path.join(self.SCRIPT_PATH, "bak"), ignore_errors=True)
+
+    def testHelp(self):
+        result = subprocess.run(
+            [self.executable, "--help"], **self.subprocess_required_args
+        )
+        self.assertIn("Script to quickly resize images.", result.stdout)
+
+    def testDefaultResizing(self):
+        subprocess.run([self.executable], **self.subprocess_required_args)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.BLACK_FILENAME)
+        ) as image_png:
+            image_png_width, image_png_height = image_png.size
+            self.assertEqual(image_png_width, 1200)
+            self.assertEqual(image_png_height, 675)
+        with PILImage.open(
+            os.path.join(self.SCRIPT_PATH, self.RED_FILENAME)
+        ) as image_jpg:
+            image_jpg_width, image_jpg_height = image_jpg.size
+            self.assertEqual(image_jpg_width, 1200)
+            self.assertEqual(image_jpg_height, 675)
+
+    def testCustomResizing(self):
+        subprocess.run(
+            [self.executable, "--width", "2000"], **self.subprocess_required_args
path: tests/e2etests.py
user: login: github-advanced-security[bot]
body: subprocess call - check for execution of untrusted input.

[Show more details](https://github.com/mrachuta/pyresizer/security/code-scanning/14)
created_at: 2025-03-03T18:40:16Z
author_association: NONE

PR comments:
user: login: github-advanced-security[bot]
created_at: 2025-02-27T21:14:24Z
body: This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on [this overview](/mrachuta/pyresizer/security/code-scanning?query=pr%3A3+is%3Aopen). Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out [the documentation](https://docs.github.com/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning). 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T21:36:58Z
body: CD workflow: failed ❌
❌ PR must contain exactly one of the required labels: major, minor, or bugfix.
❌ The following jobs (or job) failed: CI, Build Package
https://github.com/mrachuta/pyresizer/actions/runs/13576316891
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T21:41:48Z
body: CD workflow: failed ❌
❌ PR must contain exactly one of the required labels: major, minor, or bugfix.
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576387727
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T21:46:55Z
body: CD workflow: failed ❌
❌ PR must contain exactly one of the required labels: major, minor, or bugfix.
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576387727
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T21:51:07Z
body: CD workflow: succeeded ✅
✅ PR contains exactly one valid label: minor
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576515901
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:05:09Z
body: CD workflow: succeeded ✅
✅ PR label check not required, target branch is not master
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576700833
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:10:04Z
body: CD workflow: succeeded ✅
✅ PR contains exactly one valid label: minor
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576778385
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:14:14Z
body: CD workflow: succeeded ✅
✅ PR contains exactly one valid label: minor
✅ All required jobs passed.
https://github.com/mrachuta/pyresizer/actions/runs/13576847857
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:18:30Z
body: CD workflow: succeeded ✅
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
https://github.com/mrachuta/pyresizer/actions/runs/13576919273
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:26:28Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13577038720
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T22:33:51Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13577136722
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T17:03:24Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13606913870
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T17:09:21Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13606965437
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T17:13:40Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13607002219
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T17:24:20Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13607087850
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T17:27:16Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13607109995
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-01T20:26:40Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13608485916
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-02T09:22:32Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13614039402
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-02T09:25:45Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13614057547
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T18:43:02Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13638007694
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T18:52:27Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13638156874
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T18:57:46Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13638249235
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T18:58:32Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13638246334
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T18:59:26Z
body: CD workflow: succeeded ✅
URL: https://github.com/mrachuta/pyresizer/actions/runs/13638267432
- PR contains exactly one valid label: minor ✅
- All required jobs finished with status success ✅
author_association: NONE