Software diagnostics — stack traces, logging and testing — DedSec Project
Software diagnostics — stack traces, logging and testing Stack traces Practical explanation A stack trace records active call frames at a failure. Read from the triggering exception and…
Stack traces
Practical explanation
A stack trace records active call frames at a failure. Read from the triggering exception and locate the nearest relevant frame in your own code, while distinguishing cause from later propagation.
Worked illustration
def read_score(data): return int(data['score'])
def handle(data): return read_score(data)
try: handle({'score':'oops'})
except ValueError as exc: assert 'invalid' in str(exc)
Realistic exercise
Capture and inspect traceback on a disposable input. Compare original failure with a broad except that hides it, and show how to preserve exception chaining.
Review the actual contract
Describe how a caller can distinguish valid behavior for Stack traces from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.
Logging and observability
Practical explanation
Logs record events; metrics summarize numerical signals and traces connect operations across components. Combining them can identify bottlenecks, but they answer different questions and require privacy controls.
Worked illustration
import logging
logging.basicConfig(level=logging.INFO)
logging.info('batch completed count=%d',3)
Realistic exercise
Add a request correlation ID and timing metric for a synthetic batch. Keep tokens and raw personal records out of the log message.
Review the actual contract
Describe how a caller can distinguish valid behavior for Logging and observability from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.
Unit integration and end-to-end tests
Practical explanation
A unit test isolates a function, an integration test exercises boundaries such as a real database, and an end-to-end test covers a user flow. They expose different failures and have different cost/stability.
Worked illustration
def normalize(text):return text.strip().casefold()
assert normalize(' Ada ')=='ada'
Realistic exercise
Turn the assertion into unittest. Then build a SQLite integration check using a temporary database, without calling the integration check a unit test.
Review the actual contract
Describe how a caller can distinguish valid behavior for Unit integration and end to end tests from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.
Regression protection
Practical explanation
A regression test preserves the smallest case that exposed a previously fixed defect. Without checking that it failed before the fix, the new test might not guard the actual bug.
Worked illustration
def safe_ratio(a,b):
if b==0:raise ZeroDivisionError('denominator')
return a/b
assert safe_ratio(4,2)==2
Realistic exercise
Write a test requiring the expected exception for a zero denominator and compare with a previous version that returned a misleading default.
Review the actual contract
Describe how a caller can distinguish valid behavior for Regression protection from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.
Performance profiling
Practical explanation
Profiling observes where CPU time, allocations or IO waits occur. Optimizing the most visually complex function without measurement may not affect actual latency.
Worked illustration
import cProfile
cProfile.run('sum(i*i for i in range(10000))')
Realistic exercise
Profile an intentionally slow nested-loop duplicate finder and a set-based alternative. Run multiple trials and avoid conflating startup with steady-state behavior.
Review the actual contract
Describe how a caller can distinguish valid behavior for Performance profiling from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.
Unit, integration, regression and property-based testing
Separate a contract from implementation details
A good unit test asserts behavior that would remain true after an internal refactor. For a parser that accepts two comma-separated positive integers, define an explicit contract: either return both positive integers or raise ValueError for malformed input. Do not duplicate the parser logic inside the assertion, as both implementation and test could share the same mistake.
def parse_pair(raw: str) -> tuple[int, int]:
chunks = raw.split(",")
if len(chunks) != 2:
raise ValueError("expected two fields")
try:
left, right = (int(chunk.strip()) for chunk in chunks)
except ValueError as exc:
raise ValueError("invalid integer") from exc
if left <= 0 or right <= 0:
raise ValueError("both integers must be positive")
return left, right
assert parse_pair("2, 3") == (2, 3)
for malformed in ("", "2", "2,a", "0,3", "2,3,4"):
try:
parse_pair(malformed)
except ValueError:
pass
else:
raise AssertionError("accepted invalid input")
Test a case like " 2 , 3 " to specify whitespace behavior. Explain whether "+2,3" should be accepted: the current int conversion permits plus signs, so the test suite must either document that or enforce a stricter grammar.
Unit, integration and end-to-end scope
A unit test checks this small parsing contract. An integration test would check that the value reaches the database or downstream function as expected. An end-to-end test would exercise the actual user's form and error rendering. Passing a syntax check on JavaScript does not imply that mobile scrolling, visibility or input focus works.
Property-based versus examples
For generated positive pairs, parsing their decimal representation should return the same pair. But a generator restricted to valid values will miss invalid boundary cases; explicitly test empty input, non-ASCII digits, overflow policy and extra delimiters. Preserve a reproducible failing seed when generated tests expose a defect.
Regression discipline
Before a fix, write a test that reproduces the observed failure and fails for the old implementation. Only then change the code and confirm the same test passes. Keep the smallest failing fixture in version control without private user data.
Verification
Submit test command, environment/version, failures observed before the fix and successful output afterward. Do not claim a test was run merely because the snippet contains assertions; assertions are proposed checks until executed.
Logs, metrics, traces and actionable diagnostics
Logs describe discrete events; metrics summarize quantities over time; traces show causality across calls. Use structured event names, timestamps with timezone, severity levels and stable correlation identifiers. Error messages should explain what failed, where, and what to do next, without exposing credentials.
Example
import json
from datetime import datetime, timezone
record = {'event': 'backup.finished',
'time': datetime.now(timezone.utc).isoformat(),
'files': 12}
assert json.loads(json.dumps(record))['files'] == 12
Practice
- Make setup logs readable on a phone.
- Record a failed update without capturing secret environment variables.
- Add a correlation ID to a multi-step request.
- Create diagnostic output for missing dependencies without reinstalling them automatically.
Reproducing a bug
Practical explanation
A reproducible bug report contains environment, input, steps, expected behavior and actual behavior. Remove irrelevant setup until the smallest case still fails; otherwise debugging effort can chase unrelated symptoms.
Worked illustration
def broken(items=[]):
items.append(1)
return len(items)
assert broken()==1
assert broken()==2
Realistic exercise
Change the default to None and create a new list per call. Write a regression assertion that fails on the original implementation and passes on the fix.
Review the actual contract
Describe how a caller can distinguish valid behavior for Reproducing a bug from a silent failure. Check both a normal input and a boundary input, recording the source version and observed result.