It failed, but the exit code was 0
Nail the failure down as a test
Goal
Reproduce and fix a defective log rotation tool with pytest: a pure function test, tmp_path, the failing test first, parametrize, capsys, monkeypatch, and a JUnit report.
Why it matters
Saying "I fixed it" is not evidence. A test that reproduces the bug fails before the fix and passes after it, and from then on it keeps the same bug from returning. For operations tools that depend on files, time, and output, tmp_path, monkeypatch, and capsys confine those dependencies inside the test. The grader in this lab also runs your test file against the original defective code, checking in both directions that the tests truly catch the bugs.
Materials: /opt/fixtures/pyops/buggy/rotate.py is rotate.py DIR --keep N [--dry-run] [--older-than-days D]. plan(files, keep) is a pure function that returns the list of files to delete, and main(argv) returns an integer. There are two defects: (1) it keeps one more file than keep, and (2) it deletes files even with --dry-run.
Steps
- Copy it with
mkdir -p /root/pyops/test && cp /opt/fixtures/pyops/buggy/rotate.py /root/pyops/test/, and write one test in/root/pyops/test/test_rotate.pythat callsplan()directly. If keep is larger than the number of files, the result must be an empty list.pytest -qpasses. - Create 5 .log files in tmp_path and write a test that checks that 3 remain after
rotate.main([str(tmp_path), "--keep", "3"]). The function name must containkeep. This test must fail against the current code (the reproduction). - Fix plan() in rotate.py so that only keep files remain.
pytest -qpasses completely. - Create 2 files in tmp_path and write a test that checks that both files are still there after
--keep 0 --dry-run(the function name containsdry). It must fail against the original code, and you fix rotate.py to make it pass. - Write a test with
@pytest.mark.parametrizethat varies keep over 0, 1, 3, and 10 and checks that the number of remaining files is min(5, keep).pytest --collect-only -qmust show at least 4 IDs with[attached. - Write a test that uses capsys to check that
delete <파일이름>is printed to standard output (the function name containsoutputand it usescapsys). The placeholder is the file name. - Write a test that freezes
rotate.time.timewith monkeypatch and checks that--older-than-days 7deletes only the file that is 8 days old and keeps the file that is one day old (the function name containsolderand it usesmonkeypatch). Set the modification times of the files withos.utime. - Leave a report with
pytest -q --junitxml=/root/pyops/test/report.xml. There must be at least 7 tests, and failures and errors must be 0.
Notes
- Run the test file inside
/root/pyops/test/withpytest -q. If it is in the same directory as rotate.py,import rotateworks. - For the time 8 days ago, call
os.utime(path, (t, t))witht = FIXED - 8 * 86400. FIXED is the value you make time.time return with monkeypatch. - Common mistakes: a sleep that waits for real time, using the fixed path /tmp, and not checking that the test fails before the fix.
Test the pure function first
Copy rotate.py to /root/pyops/test/ and write one test in /root/pyops/test/test_rotate.py that calls plan() directly. If keep is larger than the number of files, the result must be an empty list. pytest -q passes.
After import rotate, assert rotate.plan(["a.log", "b.log"], keep=10) == []. The test function name must start with test_ for pytest to find it.
Reproduce it with a failing test
Create 5 .log files in tmp_path and write a test (with keep in its name) that checks that 3 remain after rotate.main([str(tmp_path), "--keep", "3"]). It must fail against the current code.
Create the files with (tmp_path / f"app-{i}.log").write_text("x") and look at the length of list(tmp_path.glob("*.log")). It is normal for the test to turn red because of the defect, leaving 4 files.
Fix it and confirm green
Fix plan() in rotate.py so that only keep files remain. pytest -q passes completely.
The end of the slice must be len(files) - keep. After the fix, the step 2 test must pass and the step 1 test must still pass.
Handle the second defect in the same order
Create 2 files in tmp_path and write a test (with dry in its name) that checks that both files are still there after --keep 0 --dry-run. It must fail against the original code, and you fix rotate.py to make it pass.
main() must look at args.dry_run before it calls os.remove. By convention, the output (delete ...) is printed in dry-run mode as well.
Several sets of input in one test
Write a test with @pytest.mark.parametrize that varies keep over 0, 1, 3, and 10 and checks that the number of remaining files is min(5, keep). pytest --collect-only -q shows at least 4 IDs with [ attached.
Put @pytest.mark.parametrize("keep", [0, 1, 3, 10]) above the function and accept keep as an argument. The IDs are attached like test_name[0] and test_name[1].
Output is a contract too
Write a test (with output in its name, using capsys) that uses capsys to check that delete is printed to standard output.
Put capsys in the function arguments, and after rotate.main(...), assert that "delete app-0.log" is in capsys.readouterr().out. The names of the files that were kept must not be there.
Swap out time
Write a test (with older in its name, using monkeypatch) that freezes rotate.time.time with monkeypatch and checks that --older-than-days 7 deletes only the file that is 8 days old and keeps the file that is one day old.
Freeze it with monkeypatch.setattr(rotate.time, "time", lambda: FIXED), and use os.utime(path, (t, t)) to set the file times to FIXED - 886400 and FIXED - 186400.
A report a machine can read
Leave a report with pytest -q --junitxml=/root/pyops/test/report.xml. There must be at least 7 tests, and failures and errors must be 0.
--junitxml leaves the tests, failures, and errors attributes on the testsuite element. If there is a failure, the exit code is also 1.