diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
new file mode 100644
index 00000000..9b4c0a83
--- /dev/null
+++ b/.gitea/workflows/test.yml
@@ -0,0 +1,161 @@
+name: Run Pytest with Allure and Coverage Reports
+
+on:
+ push:
+ branches: [testing_json]
+ pull_request:
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Show checked out files
+ run: |
+ echo "π Files in repo root:"
+ ls -1 | head -n 20
+
+ - name: Install Pixi & project dependencies
+ run: |
+ echo "π§ Installing Pixi..."
+ curl -fsSL https://pixi.sh/install.sh | bash > pixi.log 2>&1 || {
+ echo "β Pixi download failed"
+ cat pixi.log | grep -i 'error\|fatal'
+ exit 1
+ }
+
+ echo "$HOME/.pixi/bin" >> $GITHUB_PATH
+ export PATH="$HOME/.pixi/bin:$PATH"
+
+ pixi install > pixi-install.log 2>&1 || {
+ echo "β Pixi project dependencies failed"
+ cat pixi-install.log | grep -i 'error\|fatal'
+ exit 1
+ }
+
+ echo "β
Pixi installed"
+ pixi list
+
+ - name: Install Python test/report tools
+ run: |
+ echo "π Setting up Python environment..."
+ python -m venv venv
+ source venv/bin/activate
+
+ pip install -U pip > pip.log 2>&1 || {
+ echo "β pip upgrade failed"
+ cat pip.log | grep -i 'error\|fatal'
+ exit 1
+ }
+
+ pip install pytest pytest-cov coverage pytest-md-report > pip.log 2>&1 || {
+ echo "β Python testing tools install failed"
+ cat pip.log | grep -i 'error\|fatal'
+ exit 1
+ }
+
+ echo "β
Python tools installed"
+ pip list
+
+ - name: Install Java
+ run: |
+ echo "β Installing Java..."
+ sudo apt update > java.log 2>&1 && sudo apt install -y openjdk-11-jdk >> java.log 2>&1 || {
+ echo "β Java installation failed"
+ cat java.log | grep -i 'error\|fatal'
+ exit 1
+ }
+ echo "β
Java installed"
+ java -version
+
+ - name: Install Allure CLI
+ run: |
+ echo "π Installing Allure..."
+ curl -sL https://github.com/allure-framework/allure2/releases/download/2.27.0/allure-2.27.0.tgz | tar -xz > allure.log 2>&1 || {
+ echo "β Allure download failed"
+ cat allure.log | grep -i 'error\|fatal'
+ exit 1
+ }
+
+ sudo mv allure-2.27.0 /opt/allure
+ sudo ln -s /opt/allure/bin/allure /usr/local/bin/allure
+ echo "β
Allure installed"
+ allure --version
+
+ - name: Run tests and generate reports
+ run: |
+ echo "π Running tests and generating reports..."
+ source venv/bin/activate
+ mkdir -p ci-reports/{allure,coverage,markdown}
+
+ echo "π§ͺ Running pytest with coverage and report generation..."
+ pixi run pytest . \
+ --cov=slic \
+ --cov-report=xml:ci-reports/coverage/coverage.xml \
+ --cov-report=html:ci-reports/coverage/ \
+ --alluredir=allure-results \
+ --json-report \
+ --json-report-file=ci-reports/markdown/pytest-report.json \
+ --capture=no > ci-reports/markdown/raw-test-output.log 2>&1 && {
+ echo "β
Pytest completed"
+ echo ""
+ echo "π Markdown: ci-reports/markdown/test-report.md"
+ echo ""
+ echo "π¦ JSON: ci-reports/markdown/pytest.json"
+ } || {
+ echo ""
+ echo "β οΈ Some tests failed"
+ tail -n 20 ci-reports/markdown/raw-test-output.log | grep -i 'error\|fail' || true
+ }
+
+ echo ""
+ echo "π Generating Allure report..."
+ allure generate allure-results -o ci-reports/allure --clean > /dev/null 2>&1 && {
+ echo "β
Allure report generated"
+ } || {
+ echo "β οΈ Allure generation failed"
+ }
+
+ echo ""
+ echo "π Generating coverage summary..."
+ coverage report --format=markdown > ci-reports/markdown/coverage-summary.md && {
+ echo "β
Coverage summary generated"
+ } || {
+ echo "β οΈ Coverage generation failed"
+ }
+
+ {
+ echo ""
+ echo "π Generating tests markdown summary..."
+ python generate_test_summary.py
+ python json_to_md.py \
+ --input ci-reports/markdown/pytest-report.json \
+ --output ci-reports/markdown/TEST-REPORT.md \
+ --allure-dir ci-reports/allure/data/test-cases
+ echo "β
Tests markdown generated"
+ } || {
+ echo "β οΈ Tests markdown generation failed"
+ }
+
+ - name: Commit and push reports
+ run: |
+ echo "π€ Committing test & coverage reports to slic.git..."
+ git config user.name "ci-bot"
+ git config user.email "ci-bot@example.com"
+ git add ci-reports/markdown/TEST-REPORT.md
+ git commit -m "CI: update test and coverage reports" || echo "β οΈ Nothing to commit"
+
+ echo "π Pushing to branch 'testing_json' on slic.git..."
+ git push https://ci-token:${{ secrets.CI_TOKEN }}@gitea.psi.ch/tligui_y/slic.git HEAD:testing_json > push_main.log 2>&1 || {
+ echo "β Push to main repo failed"
+ cat push_main.log | grep -i 'error\|fatal\|refused'
+ exit 1
+ }
+ echo "β
Reports pushed to https://gitea.psi.ch/tligui_y/slic/ci-reports/"
+ env:
+ CI_TOKEN: ${{ secrets.CI_TOKEN }}
diff --git a/functions/io_utils.py b/functions/io_utils.py
new file mode 100644
index 00000000..d2099040
--- /dev/null
+++ b/functions/io_utils.py
@@ -0,0 +1,10 @@
+def read_file(path):
+ with open(path, "r", encoding="utf-8") as f:
+ return f.read()
+
+def write_file(path, content):
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(content)
+
+def cause_io_error():
+ raise IOError("Forced IO Error for testing")
diff --git a/fucntions/math_utils.py b/functions/math_utils.py
similarity index 100%
rename from fucntions/math_utils.py
rename to functions/math_utils.py
diff --git a/fucntions/string_utils.py b/functions/string_utils.py
similarity index 100%
rename from fucntions/string_utils.py
rename to functions/string_utils.py
diff --git a/json_to_md.py b/json_to_md.py
new file mode 100644
index 00000000..a29a4348
--- /dev/null
+++ b/json_to_md.py
@@ -0,0 +1,251 @@
+from collections import defaultdict
+from datetime import datetime
+import json
+import argparse
+import os
+import re
+
+def stringify(obj):
+ if obj is None or obj == "":
+ return "`None`"
+ if isinstance(obj, list):
+ return ', '.join(stringify(e) for e in obj)
+ if isinstance(obj, dict):
+ return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items())
+ return f"`{str(obj)}`"
+
+def normalize_nodeid(nodeid):
+ """Convert pytest nodeid to Allure fullName format"""
+ match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
+ if match:
+ file_part = match.group(1).replace("/", ".").replace("\\", ".")
+ func_part = match.group(2)
+ return f"{file_part}#{func_part}"
+ return None
+
+def write_json_value(f, value, indent=0):
+ prefix = " " * indent
+ if isinstance(value, dict):
+ if not value:
+ f.write(f"{prefix}{{}}\n")
+ else:
+ for k, v in value.items():
+ if isinstance(v, (dict, list)):
+ f.write(f"{prefix}{k}:\n")
+ write_json_value(f, v, indent + 1)
+ else:
+ # valeur simple, on Γ©crit sur la mΓͺme ligne
+ if v is None:
+ f.write(f"{prefix}{k}: None\n")
+ else:
+ f.write(f"{prefix}{k}: {v}\n")
+ elif isinstance(value, list):
+ if not value:
+ f.write(f"{prefix}[]\n")
+ else:
+ for item in value:
+ write_json_value(f, item, indent)
+ else:
+ if value is None:
+ f.write(f"{prefix}None\n")
+ else:
+ f.write(f"{prefix}{value}\n")
+
+
+def load_allure_metadata(allure_test_cases_dir):
+ allure_data = {}
+ if not os.path.exists(allure_test_cases_dir):
+ print(f"β Allure document untraceable: {allure_test_cases_dir}")
+ return allure_data
+
+ print(f"Loading Allure files from: {allure_test_cases_dir}")
+ for file in os.listdir(allure_test_cases_dir):
+ if file.endswith(".json"):
+ path = os.path.join(allure_test_cases_dir, file)
+ with open(path, 'r', encoding='utf-8') as f:
+ try:
+ data = json.load(f)
+ full_name = data.get("fullName")
+ if not full_name:
+ continue
+ params = data.get("parameters", [])
+ severity = data.get("extra", {}).get("severity", None)
+ allure_data[full_name] = {
+ "parameters": params,
+ "severity": severity
+ }
+ except Exception as e:
+ print(f"β Error in {file}: {e}")
+ return allure_data
+
+def json_to_md_nested(json_path, md_path, allure_dir=None):
+ with open(json_path) as f:
+ data = json.load(f)
+
+ allure_data = load_allure_metadata(allure_dir) if allure_dir else {}
+
+ with open(md_path, 'w', encoding='utf-8') as f:
+ f.write(f"# π§ͺ Test Report\n")
+ f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
+
+ if 'summary' in data:
+ f.write("## π Summary\n")
+ for key, value in data['summary'].items():
+ f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
+ duration = data.get("duration")
+ f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n")
+ f.write("\n")
+
+ if "tests" in data and "summary" in data:
+ test_counter = 1
+ for test in data["tests"]:
+ test['global_number'] = test_counter
+ test_counter += 1
+
+ f.write("## π Tests\n")
+
+ summary_items = list(data["summary"].items())
+ total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
+ status_order = [k for k, _ in summary_items[:total_index]]
+
+ tests_by_status = defaultdict(list)
+ for test in data["tests"]:
+ outcome = test.get("outcome", "unknown")
+ tests_by_status[outcome].append(test)
+
+ for status in status_order:
+ if status not in tests_by_status:
+ continue
+
+ count = len(tests_by_status[status])
+ emoji = "β
" if status == "passed" else "β"
+ status_label = status.capitalize().replace('_', ' ')
+ f.write(f"\n{emoji} {status_label} ({count})
\n\n")
+
+ grouped = defaultdict(lambda: defaultdict(list))
+ for test in tests_by_status[status]:
+ nodeid = test.get("nodeid", "")
+ parts = nodeid.split("::")
+ filename = parts[0].replace("tests\\", "").replace("tests/", "")
+ funcname = parts[1].split("[")[0]
+ grouped[filename][funcname].append(test)
+
+ for filename, funcs in grouped.items():
+ f.write(f"\nπ {filename}
\n\n")
+ for funcname, tests in funcs.items():
+ f.write(f"\nπ§ Function: `{funcname}`
\n\n")
+ for test in sorted(tests, key=lambda x: x['global_number']):
+ f.write(f"\n{emoji} #{test['global_number']}
\n\n")
+
+ nodeid = test.get("nodeid", "")
+ f.write(f"- **Status:** {emoji} `{status}`\n")
+ duration = test.get("call", {}).get("duration")
+ f.write(f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n")
+
+ full_name = normalize_nodeid(nodeid)
+ allure_info = allure_data.get(full_name)
+ if allure_info:
+ if allure_info["parameters"]:
+ f.write("- **Parameters (Allure):**\n")
+ for param in allure_info["parameters"]:
+ name = param.get("name")
+ val = param.get("value")
+ f.write(f" - `{name}` = `{val}`\n")
+ f.write("\n")
+ if allure_info["severity"]:
+ f.write(f"- **Severity:** `{allure_info['severity']}`\n")
+
+ for phase in ['setup', 'call', 'teardown']:
+ if phase in test:
+ f.write(f"\n### π§ {phase.capitalize()} Phase\n\n")
+ for field, value in test[phase].items():
+ if value is None:
+ f.write(f"- **{field.capitalize()}:** None\n")
+ else:
+ f.write(f"\nπ {field.capitalize()}
\n\n")
+ f.write("```\n")
+ write_json_value(f, value)
+ f.write("```\n")
+ f.write(" \n\n")
+
+ f.write(" \n\n")
+ f.write(" \n\n")
+ f.write(" \n\n")
+ f.write(" \n\n")
+
+ if "collectors" in data:
+ f.write("## π Collected files\n")
+
+ grouped = defaultdict(list)
+ for collector in data["collectors"]:
+ nodeid = collector.get("nodeid", "unknown")
+ path = nodeid.split("::")[0]
+ main_folder = path.split("/")[0] if "/" in path else path
+ grouped[main_folder].append(collector)
+
+ for folder, collectors in grouped.items():
+ has_fail = any(c.get("outcome") != "passed" for c in collectors)
+ folder_emoji = "β
" if not has_fail else "β"
+ f.write(f"\n{folder_emoji} {folder} ({len(collectors)} tests)
\n\n")
+
+ outputs = []
+ for collector in collectors:
+ outcome = collector.get("outcome", "unknown")
+ nodeid = collector.get("nodeid", "unknown")
+ short_node = nodeid.split("[")[0]
+ results = collector.get("result", [])
+ if outcome != "passed" and results:
+ outputs.append(f"### β {short_node}\n```\n" + "\n".join(
+ f"{k}: {v}" if isinstance(item, dict) else str(item)
+ for item in results
+ for k, v in item.items() if isinstance(item, dict)
+ ) + "\n```")
+
+ if outputs:
+ f.write("### π§Ύ Error or Result Summary\n\n")
+ for out in outputs:
+ f.write(out + "\n")
+
+ collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
+ for collector in collectors_sorted:
+ outcome = collector.get("outcome", "unknown")
+ emoji = "β
" if outcome == "passed" else "β"
+ nodeid = collector.get("nodeid", "unknown")
+ short_node = nodeid.split("[")[0]
+ f.write(f"\n{emoji} {short_node}
\n\n")
+ f.write(f"- **Outcome:** `{outcome}`\n")
+
+ other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
+ if other_keys:
+ f.write("- **Details:**\n")
+ f.write("```\n")
+ for k, v in other_keys.items():
+ f.write(f"{k}:\n")
+ try:
+ if v is None:
+ f.write(" None\n")
+ else:
+ write_json_value(f, v, indent=1)
+ except Exception as e:
+ f.write(f" \n")
+ f.write("\n")
+ f.write("```\n")
+ else:
+ f.write("- **Details:** `None`\n")
+
+ f.write(" \n\n")
+
+ f.write(" \n\n")
+
+def main():
+ parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
+ parser.add_argument("--input", required=True, help="Path to pytest JSON file")
+ parser.add_argument("--output", required=True, help="Path to output Markdown file")
+ parser.add_argument("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
+ args = parser.parse_args()
+
+ json_to_md_nested(args.input, args.output, args.allure_dir)
+ print(f"β
Report generated at {args.output}")
+
+if __name__ == "__main__":
+ main()
diff --git a/pixi.toml b/pixi.toml
new file mode 100644
index 00000000..412a459e
--- /dev/null
+++ b/pixi.toml
@@ -0,0 +1,17 @@
+[project]
+name = "slic"
+version = "0.1.0"
+channels = ["conda-forge", "paulscherrerinstitute"]
+platforms = ["linux-64"]
+
+[dependencies]
+python = "3.8.*"
+pytest = "*"
+coverage = "*"
+pytest-html = "*"
+pytest-cov = "*"
+allure-pytest = "*"
+
+[pypi-dependencies]
+pytest-md-report = "*"
+pytest-json-report = "*"
diff --git a/tests/test_broken_fct.py b/tests/test_broken_fct.py
new file mode 100644
index 00000000..4981f98b
--- /dev/null
+++ b/tests/test_broken_fct.py
@@ -0,0 +1,10 @@
+def test_valid_1():
+ # To see if it's ignored
+ assert True
+
+def test_broken(
+# syntax error
+
+def test_valid_2():
+ # To see if it's ignored
+ assert True
diff --git a/tests/test_io_utils.py b/tests/test_io_utils.py
new file mode 100644
index 00000000..613a1738
--- /dev/null
+++ b/tests/test_io_utils.py
@@ -0,0 +1,75 @@
+import pytest
+import functions.io_utils
+import os
+import warnings
+import io
+from unittest import mock
+
+def test_read_file(tmp_path):
+ # Write and read file, normal operation
+ file = tmp_path / "test.txt"
+ file.write_text("hello")
+ content = io_utils.read_file(str(file))
+ assert content == "hello"
+
+def test_write_file(tmp_path):
+ # Write content to file and verify write success
+ file = tmp_path / "test_write.txt"
+ io_utils.write_file(str(file), "data")
+ assert file.read_text() == "data"
+
+def test_cause_io_error():
+ # Raises manual IOError to simulate IO failure
+ with pytest.raises(IOError):
+ io_utils.cause_io_error()
+
+def test_file_not_found():
+ # Reading non-existing file raises FileNotFoundError
+ with pytest.raises(FileNotFoundError):
+ io_utils.read_file("nonexistent.file")
+
+def test_permission_error(monkeypatch):
+ # Patch open to raise PermissionError simulating access denial
+ def raise_perm_error(*args, **kwargs):
+ raise PermissionError("Permission denied")
+ monkeypatch.setattr("builtins.open", raise_perm_error)
+ with pytest.raises(PermissionError):
+ io_utils.read_file("anyfile.txt")
+
+def test_mock_open_error(monkeypatch):
+ # Mock open() to raise IOError simulating read error
+ mocked_open = mock.mock_open()
+ mocked_open.side_effect = IOError("Mocked IOError")
+ monkeypatch.setattr("builtins.open", mocked_open)
+ with pytest.raises(IOError):
+ with open("file.txt", "r") as f:
+ f.read()
+
+def test_file_handle_closed_error():
+ # Accessing closed file raises ValueError
+ f = io.StringIO("content")
+ f.close()
+ with pytest.raises(ValueError):
+ f.read()
+
+def test_os_error(monkeypatch):
+ # Patch os.remove to raise OSError simulating filesystem error
+ def raise_os_error(path):
+ raise OSError("Simulated OSError")
+ monkeypatch.setattr("os.remove", raise_os_error)
+ with pytest.raises(OSError):
+ os.remove("file.txt")
+
+def test_write_file_readonly(tmp_path):
+ # Writing to read-only file raises PermissionError
+ file = tmp_path / "readonly.txt"
+ file.write_text("data")
+ os.chmod(file, 0o444)
+ with pytest.raises(PermissionError):
+ with open(file, "w") as f:
+ f.write("new content")
+
+def test_file_not_found_error():
+ # Raises FileNotFoundError when opening a non-existent file
+ with pytest.raises(FileNotFoundError):
+ open("no_such_file.txt", "r")
diff --git a/tests/test_math_utils.py b/tests/test_math_utils.py
new file mode 100644
index 00000000..255bd48e
--- /dev/null
+++ b/tests/test_math_utils.py
@@ -0,0 +1,145 @@
+import pytest
+import functions.math_utils
+import math
+import sys
+import importlib
+from unittest import mock
+
+class CustomError(Exception):
+ pass
+
+def test_broken():
+ # Simulate an invalid syntaxe test
+ want_the_test_to_fail
+
+def test_call_missing_function():
+ # Accessing a missing function attribute raises AttributeError
+ with pytest.raises(AttributeError):
+ getattr(math_utils, "non_existent_function")()
+
+def test_addition_pass():
+ # Test passes: correct addition
+ assert math_utils.addition(2, 2) == 4
+
+def test_addition_fail():
+ # Assertion failure: expected incorrect result
+ assert math_utils.addition(2, 2) == 5
+
+def test_division_zero():
+ # Expect ZeroDivisionError when dividing by zero
+ with pytest.raises(ZeroDivisionError):
+ math_utils.division(1, 0)
+
+@pytest.mark.xfail(reason="Expected failure")
+def test_multiply_xfail():
+ # Expected fail test (xfail): incorrect expected multiply result
+ assert math_utils.multiply(2, 2) == 5
+
+def test_runtime_error():
+ # Test raises an uncaught RuntimeError
+ raise RuntimeError("Forced runtime error")
+
+def test_memory_error():
+ # Manually raise MemoryError to simulate out-of-memory condition
+ with pytest.raises(MemoryError):
+ raise MemoryError("Simulated memory error")
+
+def test_timeout_error():
+ # Manually raise TimeoutError simulating timeout conditions
+ with pytest.raises(TimeoutError):
+ raise TimeoutError("Simulated timeout error")
+
+def test_recursion_error():
+ # Infinite recursion triggers RecursionError
+ def recursive():
+ return recursive()
+ with pytest.raises(RecursionError):
+ recursive()
+
+def test_floating_point_error():
+ # Manually raise FloatingPointError (rare in practice)
+ with pytest.raises(FloatingPointError):
+ raise FloatingPointError("Simulated floating point error")
+
+def test_floating_point_overflow():
+ # Exponential overflow triggers OverflowError
+ with pytest.raises(OverflowError):
+ math.exp(1000)
+
+def test_value_error():
+ # ValueError on invalid integer conversion
+ with pytest.raises(ValueError):
+ int("invalid")
+
+def test_type_error():
+ # TypeError when passing wrong argument type to sum
+ with pytest.raises(TypeError):
+ sum(5)
+
+def test_unhandled_exception():
+ # Raises generic unhandled Exception
+ raise Exception("Generic unhandled exception")
+
+def test_custom_error():
+ # Raises user-defined CustomError exception
+ with pytest.raises(CustomError):
+ raise CustomError("Custom error simulation")
+
+def test_stop_iteration_direct():
+ # Directly raise StopIteration exception
+ raise StopIteration()
+
+def test_generator_exit_direct():
+ # Directly raise GeneratorExit exception
+ raise GeneratorExit()
+
+def test_keyboard_interrupt_direct():
+ # Directly raise KeyboardInterrupt exception
+ raise KeyboardInterrupt()
+
+def test_recursion_limit(monkeypatch):
+ # Lower recursion limit to force RecursionError on deep recursion
+ original_limit = sys.getrecursionlimit()
+ sys.setrecursionlimit(50)
+ def recurse():
+ return recurse()
+ with pytest.raises(RecursionError):
+ recurse()
+ sys.setrecursionlimit(original_limit)
+
+def test_malformed_code():
+ # SyntaxError when executing malformed Python code
+ with pytest.raises(SyntaxError):
+ exec("def bad(:\n pass")
+
+def test_sys_exit(monkeypatch):
+ # Simulate SystemExit via patched sys.exit
+ def fake_exit(code=0):
+ raise SystemExit(f"Exit with code {code}")
+ monkeypatch.setattr(sys, "exit", fake_exit)
+ with pytest.raises(SystemExit):
+ sys.exit(1)
+
+def test_broken_function(monkeypatch):
+ # Simulate broken function raising TypeError
+ def broken_func(*args, **kwargs):
+ raise TypeError("Broken function")
+ monkeypatch.setattr(__name__, "test_broken_function", broken_func)
+ with pytest.raises(TypeError):
+ broken_func()
+
+def test_import_error_patch(monkeypatch):
+ # Patch import to simulate ImportError on specific module
+ original_import = __import__
+ def fake_import(name, *args, **kwargs):
+ if name == "fake_module":
+ raise ImportError("Simulated ImportError")
+ return original_import(name, *args, **kwargs)
+ monkeypatch.setattr("builtins.__import__", fake_import)
+ with pytest.raises(ImportError):
+ __import__("fake_module")
+
+def test_module_not_found_error():
+ # Raises ModuleNotFoundError (subclass of ImportError) for missing module
+ with pytest.raises(ModuleNotFoundError):
+ importlib.import_module("non_existent_module_xyz")
diff --git a/tests/test_string_utils.py b/tests/test_string_utils.py
new file mode 100644
index 00000000..1903fee4
--- /dev/null
+++ b/tests/test_string_utils.py
@@ -0,0 +1,47 @@
+import pytest
+import functions.string_utils
+import warnings
+
+def test_uppercase_normal():
+ # Normal string uppercase conversion
+ assert string_utils.uppercase("hello") == "HELLO"
+
+def test_uppercase_type_error():
+ # Raises TypeError when input is None (invalid input)
+ with pytest.raises(TypeError):
+ string_utils.uppercase(None)
+
+def test_reverse_string():
+ # Tests string reversal correctness
+ assert string_utils.reverse("abc") == "cba"
+
+def test_warning_emit():
+ # Emits a Python UserWarning, checks warning capture
+ warnings.warn("Test warning", UserWarning)
+
+def test_unicode_encode_error():
+ # UnicodeEncodeError due to decoding malformed surrogate byte
+ with pytest.raises(UnicodeEncodeError):
+ b'\udc80'.decode('utf-8')
+
+def test_unicode_decode_error():
+ # UnicodeDecodeError when decoding invalid byte sequence
+ with pytest.raises(UnicodeDecodeError):
+ b'\xff'.decode('utf-8')
+
+def test_unicode_decode_surrogateescape():
+ # UnicodeDecodeError with strict error handler on invalid byte
+ with pytest.raises(UnicodeDecodeError):
+ b"\x80".decode("utf-8", errors="strict")
+
+def test_import_warning():
+ # Capture and check ImportWarning emitted
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ warnings.warn("Import warning", ImportWarning)
+ assert any(item.category == ImportWarning for item in w)
+
+@pytest.mark.xfail(reason="Expected failure: uppercase does not handle digits")
+def test_xfail_uppercase_digits():
+ # Expected fail test because uppercase won't change digits
+ assert string_utils.uppercase("abc123") == "ABC1234"