From e42db69dd75a771b3668bbcbf7115e9e4300070a Mon Sep 17 00:00:00 2001 From: Laraib Ahmad Siddiqui <46929194+mrsddq@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:59:24 +0530 Subject: [PATCH] Test reference algorithms and fix stable sorting contracts --- .github/workflows/ci.yml | 18 +++++++++++ Makefile | 8 ++--- README.md | 25 +++++++++++++-- buildspec.yml | 8 +++-- docs/engineering-runbook.md | 53 +++++++++---------------------- pyproject.toml | 4 +++ src/dsa/sorting.py | 4 +-- tests/test_invariants.py | 62 +++++++++++++++++++++++++++++++++++++ 8 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 pyproject.toml create mode 100644 tests/test_invariants.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..21b1d6c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: Reference algorithms +on: [push, pull_request] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: pip + - run: python -m pip install -r requirements-dev.txt + - run: make test diff --git a/Makefile b/Makefile index d99ea4b..d221399 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ -.PHONY: install test compile clean +.PHONY: install test compile clean install: - python -m pip install --upgrade pip + python -m pip install -r requirements-dev.txt test: - python -m unittest discover -s tests + python -m pytest compile: - python -m compileall -q . + python -m compileall -q src clean: python -c "import shutil; from pathlib import Path; [shutil.rmtree(p, ignore_errors=True) for p in Path('.').rglob('__pycache__')]" diff --git a/README.md b/README.md index 35a5829..30e09da 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,31 @@ docs/ ## Clean Reference Implementations +Requires Python 3.11+. From the repository root: + ```bash -pytest +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +python -m pip install -r requirements-dev.txt +python -m pytest ``` +`make test` runs the same pytest suite. Collection is restricted to `tests/`: +original exercises may read stdin or run demonstrations on import. + +The reference suite checks exhaustive small input combinations against Python's +built-in search/sort behavior, stable sorting with custom `__lt__` objects, +non-mutation, and stack/queue lifecycle errors. GitHub Actions exercises Python +3.11–3.13; AWS CodeBuild uses the same tests. + +| Operation | Time | Extra space | Contract | +| --- | --- | --- | --- | +| Linear search | O(n) | O(1) | First matching index, or -1 | +| Binary search | O(log n) | O(1) | Ascending input; any matching index, or -1 | +| Merge sort | O(n log n) | O(n) | Stable new list; mutually orderable elements | +| Stack push / pop | O(1) amortized | O(n) storage | LIFO; empty pop/peek raises IndexError | +| Queue enqueue / dequeue | O(1) | O(n) storage | FIFO; empty dequeue raises IndexError | + Current reference modules: - `src/dsa/searching.py` @@ -47,4 +68,4 @@ For every topic: This is the primary home for Python algorithm, DSA, and revision work. -Next upgrade: expand edge-case tests and add time/space complexity notes for each clean implementation in `src/dsa/`. +The archived exercises are historical learning material; the supported, CI-tested surface is `src/dsa/`. diff --git a/buildspec.yml b/buildspec.yml index f9869a0..d2fef68 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -1,6 +1,10 @@ version: 0.2 - phases: + install: + runtime-versions: + python: 3.12 + commands: + - python -m pip install -r requirements-dev.txt build: commands: - # - echo "Hello World" + - python -m pytest diff --git a/docs/engineering-runbook.md b/docs/engineering-runbook.md index 56a887b..9fb6c6c 100644 --- a/docs/engineering-runbook.md +++ b/docs/engineering-runbook.md @@ -1,39 +1,14 @@ -# Engineering Runbook - -## Repository Profile - -- Repository: $repoName -- Classification: Python project -- Tracked files: 111 -- Python files: 76 -- JavaScript/TypeScript files: 0 -- Notebooks: 0 -- Terraform files: 0 - -## Setup - -``bash -No package install step is required for the tracked source. -`` - -## Verification - -``bash -python -m unittest discover -s tests -python -m compileall -q . -`` - -## Release Hygiene - -- Keep generated outputs, caches, local datasets, virtual environments, and dependency folders out of git. -- Prefer deterministic commands over manual notebook or console-only steps. -- Document required secrets and environment variables instead of committing them. -- Keep Dockerfiles, CI workflows, and tests aligned with the actual project stack. -- Treat learning or reference material honestly as reference material; do not present it as production service code unless it has service-grade tests, deployment, and operations docs. - -## Maintenance Checklist - -- Review dependencies quarterly. -- Run tests before every push. -- Confirm git status --short is clean before packaging. -- Include .git only when an external submission explicitly requires repository history. +# Reference algorithm verification + +```bash +python -m pip install -r requirements-dev.txt +make test +make compile +``` + +Pytest collection is limited to `tests/`; historical exercises can run input or +print operations on import. Add new maintained algorithms under `src/dsa/` and +cover empty inputs, duplicates, non-mutation, and declared ordering contracts. +Binary search assumes ascending data and may return any duplicate match. +Exhaustive small-domain tests compare search and sorting results to built-in +oracles; queue and stack tests exercise reuse and underflow. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6547a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +addopts = "-ra" diff --git a/src/dsa/sorting.py b/src/dsa/sorting.py index 1809d51..435db8c 100644 --- a/src/dsa/sorting.py +++ b/src/dsa/sorting.py @@ -1,5 +1,5 @@ def merge_sort(values): - """Return a sorted copy of values using merge sort.""" + """Return a stable sorted copy; O(n log n) time and O(n) auxiliary space.""" if len(values) <= 1: return list(values) @@ -15,7 +15,7 @@ def _merge(left, right): j = 0 while i < len(left) and j < len(right): - if left[i] <= right[j]: + if not right[j] < left[i]: result.append(left[i]) i += 1 else: diff --git a/tests/test_invariants.py b/tests/test_invariants.py new file mode 100644 index 0000000..b6b2147 --- /dev/null +++ b/tests/test_invariants.py @@ -0,0 +1,62 @@ +from itertools import product + +import pytest + +from src.dsa.queue import Queue +from src.dsa.searching import binary_search, linear_search +from src.dsa.sorting import merge_sort +from src.dsa.stack import Stack + + +def test_sort_and_search_against_builtin_oracles(): + for length in range(6): + for values in product((-1, 0, 1), repeat=length): + ordered = merge_sort(values) + assert ordered == sorted(values) + for target in (-2, -1, 0, 1, 2): + index = binary_search(ordered, target) + assert (index == -1) == (target not in ordered) + if index != -1: + assert ordered[index] == target + expected = values.index(target) if target in values else -1 + assert linear_search(values, target) == expected + + +def test_merge_sort_preserves_equal_key_order_with_lt_only_objects(): + class Record: + def __init__(self, key, name): + self.key, self.name = key, name + + def __lt__(self, other): + return self.key < other.key + + records = [Record(2, "a"), Record(1, "b"), Record(2, "c"), Record(1, "d")] + original = records[:] + assert [record.name for record in merge_sort(records)] == ["b", "d", "a", "c"] + assert records == original + + +def test_interleaved_queue_operations_and_reuse_after_empty(): + queue = Queue() + queue.enqueue(None) + queue.enqueue("a") + assert queue.dequeue() is None + queue.enqueue("b") + assert [queue.dequeue(), queue.dequeue()] == ["a", "b"] + assert queue.is_empty() and len(queue) == 0 + with pytest.raises(IndexError): + queue.dequeue() + queue.enqueue("reused") + assert queue.dequeue() == "reused" + + +def test_stack_peek_does_not_consume_and_empty_errors(): + stack = Stack() + for operation in (stack.peek, stack.pop): + with pytest.raises(IndexError): + operation() + for value in (None, "a", "b"): + stack.push(value) + assert stack.peek() == "b" and len(stack) == 3 + assert [stack.pop(), stack.pop(), stack.pop()] == ["b", "a", None] + assert stack.is_empty()