TT Lab
Get started
Learn Learning paths Courses

It failed, but the exit code was 0

Nail the failure down as a test

Continue in TT Lab

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

  1. 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.py that calls plan() directly. If keep is larger than the number of files, the result must be an empty list. pytest -q passes.
  2. 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 contain keep. This test must fail against the current code (the reproduction).
  3. Fix plan() in rotate.py so that only keep files remain. pytest -q passes completely.
  4. 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 contains dry). It must fail against the original code, and you fix rotate.py to make it pass.
  5. 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 must show at least 4 IDs with [ attached.
  6. Write a test that uses capsys to check that delete <파일이름> is printed to standard output (the function name contains output and it uses capsys). The placeholder is the file name.
  7. Write a test 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 (the function name contains older and it uses monkeypatch). Set the modification times of the files with os.utime.
  8. 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

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.