~90 minutes. Pick ONE prior lab (6, 9, 11, or 12). Add three or more pytest tests. Write a five-section README. Ship it as if you would hand it to a stranger.
Goal: add tests/test_*.py with three or more passing tests AND a README.md to one of your prior labs. Commit both.
Estimated time: 90 minutes
Prerequisites: Week 13 lecture (pytest, README discipline). The chosen prior lab complete and working.
Setup
Pick which prior lab you will add tests + README to:
- Lab 6 (log scanner with argparse + logging): rich enough; the
scangenerator +format_size-style helpers are testable - Lab 9 (du wrapper): testable parsing logic; the subprocess wrapping needs mocking (harder)
- Lab 11 (integrity checker): excellent fit;
hash_file,build_manifest,verifyare all pure functions - Lab 12 (weather CLI): testable if you mock
requests.get; harder than Lab 11
Lab 11 is the recommended pick for most students: the functions are pure (no I/O at the boundary), making them easy to test. The instructions below use Lab 11 as the worked example; substitute your chosen lab as needed.
Create the lab-13 directory and copy your chosen prior lab:
mkdir -p ~/fnd-102/lab-13
cd ~/fnd-102/lab-13
cp ../lab-11/lab-11-checker.py checker.py
mkdir tests
Install pytest if you haven't:
python3 -m pip install --user pytest
Part A: Write the first test (15 min)
tests/test_checker.py:
"""Tests for lab-11-checker (now copied to checker.py for lab-13)."""
import hashlib
import json
from pathlib import Path
import pytest
from checker import hash_file, build_manifest, verify, write_manifest, read_manifest
def test_hash_file_known_value(tmp_path):
"""The hash of 'hello' (no newline) is a known SHA-256 value."""
f = tmp_path / 'data.bin'
f.write_bytes(b'hello')
expected = hashlib.sha256(b'hello').hexdigest()
assert hash_file(f) == expected
Notice:
tmp_pathis a pytest built-in fixture that gives a fresh temp directory.hashlib.sha256(b'hello').hexdigest()lets us assert against the known correct value WITHOUT hardcoding the long hex string in the test.from checker import ...assumes the test is run from~/fnd-102/lab-13(the project root); pytest's default is to add the test's parent directories tosys.pathso this works.
Run:
cd ~/fnd-102/lab-13
pytest
If you see ModuleNotFoundError: No module named 'checker', you renamed the file from lab-11-checker.py to checker.py (good); pytest finds it because the current directory is on sys.path. If pytest does not find it, add an empty __init__.py to the test directory and an explicit conftest.py in the project root.
Part B: Write the second test (15 min)
Test the manifest-building function:
def test_build_manifest_three_files(tmp_path):
"""Building a manifest of a 3-file tree produces 3 entries with the right hashes."""
(tmp_path / 'a.txt').write_text('apple')
(tmp_path / 'b.txt').write_text('banana')
(tmp_path / 'sub').mkdir()
(tmp_path / 'sub' / 'c.txt').write_text('cherry')
manifest = build_manifest(tmp_path)
assert len(manifest) == 3
assert 'a.txt' in manifest
assert 'b.txt' in manifest
assert 'sub/c.txt' in manifest
assert manifest['a.txt'] == hashlib.sha256(b'apple').hexdigest()
The test covers:
- Counting: 3 files become 3 manifest entries
- Path handling: subdirectories are joined with
/correctly (POSIX style) - Hashing: the manifest's hash matches a known reference
Run again; both tests should pass.
Part C: Write the third test (a regression test) (15 min)
A regression test guards against a bug that was found and fixed. Either find a real bug (try to break your verify function) or pick a known edge case:
def test_verify_detects_modified_added_deleted(tmp_path):
"""A modified, an added, and a deleted file are all detected by verify."""
# Initial tree
(tmp_path / 'a.txt').write_text('apple')
(tmp_path / 'b.txt').write_text('banana')
manifest_path = tmp_path / 'manifest.json'
write_manifest(build_manifest(tmp_path), manifest_path)
# Modify a.txt; add c.txt; delete b.txt
(tmp_path / 'a.txt').write_text('apricot')
(tmp_path / 'c.txt').write_text('cherry')
(tmp_path / 'b.txt').unlink()
# ALSO delete the manifest.json so verify does not see it
manifest_path.rename(tmp_path.parent / 'manifest.json')
result = verify(tmp_path, tmp_path.parent / 'manifest.json')
assert 'a.txt' in result['modified']
assert 'c.txt' in result['added']
assert 'b.txt' in result['deleted']
# ensure manifest.json itself is not somehow in the results
assert 'manifest.json' not in result['added']
This test is intentionally exhaustive: it exercises all three change categories in one test. Some teams prefer one assertion per test (smaller failure surface); others prefer integration-style tests like this one (clearer "what scenario broke?"). Pick the style that fits your team.
Run; all three should pass.
Part D: Add a parametrized test (10 min)
For variety, add a parametrized test using @pytest.mark.parametrize:
@pytest.mark.parametrize('content, expected_hash', [
(b'', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'), # empty
(b'a', 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'), # single byte
(b'hello', '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'), # short string
])
def test_hash_file_various_contents(tmp_path, content, expected_hash):
"""Hash of various inputs matches known SHA-256 reference values."""
f = tmp_path / 'data.bin'
f.write_bytes(content)
assert hash_file(f) == expected_hash
Run; you now have at least 6 test cases (3 from parametrize + 3 named tests). pytest reports each parametrize case separately.
Verify the known hashes by running echo -n 'hello' | sha256sum from the shell; the output should match the hex string.
Part E: Write the README (25 min)
README.md in ~/fnd-102/lab-13/:
# Directory Integrity Checker
A small CLI tool that computes SHA-256 hashes for every file in a directory
tree, saves them as a manifest, and later detects which files have been
modified, added, or deleted. Structurally similar to (but simpler than) AIDE
and Tripwire.
## Install
```bash
git clone https://github.com/YOUR-USERNAME/fnd-102.git
cd fnd-102/lab-13
python3 -m pip install --user -r requirements.txt
Requires Python 3.11+ and pytest (for tests; no third-party deps for the tool itself).
Usage
Write a manifest:
python3 checker.py manifest /path/to/tree --out manifest.json
# Wrote manifest with 47 files to manifest.json
Verify the tree against the manifest later:
python3 checker.py verify /path/to/tree --manifest manifest.json
# OK: all files match the manifest
# Or, after changes:
# CHANGES DETECTED:
# MODIFIED src/main.py
# ADDED src/new-file.py
# DELETED docs/README.md
Exit codes: 0 if clean, 1 if changes detected.
Tests
pytest
License
MIT (or whatever; if this is just a course artifact, no formal license needed; mention "course artifact for VCA-FND-102 at Virtus Cyber Academy").
Write `requirements.txt`:
pytest>=7.0
(That is it; the tool itself uses only the stdlib.)
---
## Part F: Verify the stranger experience (5 min)
Close your editor. Open a new terminal. `cd ~/fnd-102/lab-13`. Read the README from the top. Follow the install instructions; run the tests; run the tool. Does anything trip you up?
If it does, fix the README. The discipline: the README is the contract between you and your future self / a stranger.
---
## Part G: Commit your work (10 min)
```bash
cd ~/fnd-102/lab-13
git add checker.py tests/test_checker.py README.md requirements.txt
git commit -m "lab-13: add pytest tests + README to integrity checker
Six test cases covering hash_file, build_manifest, and verify (modified /
added / deleted detection). Five-section README that lets a stranger
clone, install, and run the tool. requirements.txt with pytest as the
only third-party dependency."
Expected output / artifact
~/fnd-102/lab-13/checker.py(the tool, copied from your chosen prior lab)~/fnd-102/lab-13/tests/test_checker.pywith ≥3 test functions (one parametrized counts as multiple cases)~/fnd-102/lab-13/README.mdwith the 5 sections (what / install / use / tests / license)~/fnd-102/lab-13/requirements.txt- All tests pass when you run
pytestfrom~/fnd-102/lab-13
What's the failure mode?
This lab's likely failure modes:
- Tests pass locally but fail in CI. Almost always a path issue: your test assumes a file location that differs in CI. Always use
tmp_pathfor filesystem state in tests; never read or write to the working directory directly. - README says one thing; code does another. Drift between docs and code. The fix is to RUN the README's commands as part of your test suite (a
make verifytarget, or a CI job that exercises the README's install/run steps). - Test takes 60 seconds. A test that hits the network, calls subprocess, or hashes a 100 MB file is too slow to run on every commit. Use
pytest.mark.slowto tag it and skip by default; run weekly. pytestnot installed in CI.requirements.txtsolves this. Some teams userequirements-dev.txtfor dev-only dependencies (pytest, ruff); split if your team's convention says so.
Common pitfalls
import checkerfails with ModuleNotFoundError. Either pytest is being run from the wrong directory, orchecker.pyis not onsys.path. Run pytest from the project root; ensure there is no conflictingconftest.pyinterfering.- A test passes for the wrong reason. A test that
assert Truealways passes; that does not mean the code under test works. Make sure each test actually exercises the function under test. - Mutable shared state between tests. A test that writes to a module-level list leaves state for the next test. Use fixtures +
tmp_pathto give each test fresh state. - README installs that do not work. Test them on a fresh machine (or fresh container) before merging. The "works on my machine" anti-pattern lives in READMEs as much as in code.
Stretch (optional)
- Add a
pytest-covreport.pip install pytest-cov; pytest --cov=checker. See which lines are not exercised; write a test for the most important uncovered line. - Add a GitHub Actions workflow.
.github/workflows/test.ymlthat runspyteston every push. ~20 lines of YAML; gives you green-CI feedback every commit. - Add a
Makefilewithmake test,make lint,make installtargets. Many teams use it; the convention pre-dates the modern Python tooling. - Add
rufffor linting (pip install ruff; ruff check checker.py). Catches common style issues automatically. Adds another dev-dependency. - Write a CHANGELOG.md. Document what changed in each "release" (here, each commit on main). Forward-pointer to semver and tagged releases (CSA-101 and beyond).
Lab 13 v0.1.