summaryrefslogtreecommitdiff
path: root/docs/sessions/refactor.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/sessions/refactor.org')
-rw-r--r--docs/sessions/refactor.org593
1 files changed, 593 insertions, 0 deletions
diff --git a/docs/sessions/refactor.org b/docs/sessions/refactor.org
new file mode 100644
index 00000000..11ff0a91
--- /dev/null
+++ b/docs/sessions/refactor.org
@@ -0,0 +1,593 @@
+#+TITLE: Test-Driven Quality Engineering Session: music-config.el
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2025-11-01
+
+* Overview
+
+This document describes a comprehensive test-driven quality engineering session for =music-config.el=, an EMMS music player configuration module. The session demonstrates systematic testing practices, refactoring for testability, bug discovery through tests, and decision-making processes when tests fail.
+
+* Session Goals
+
+1. Add comprehensive unit test coverage for testable functions in =music-config.el=
+2. Discover and fix bugs through systematic testing
+3. Follow quality engineering principles from =ai-prompts/quality-engineer.org=
+4. Demonstrate refactoring patterns for testability
+5. Document the decision-making process for test vs production code issues
+
+* Phase 1: Feature Addition with Testability in Mind
+
+** The Feature Request
+
+Add functionality to append a track from the EMMS playlist to an existing M3U file by pressing ~A~ on the track.
+
+Requirements:
+- Show completing-read with available M3U playlists
+- Allow cancellation (C-g and explicit "(Cancel)" option)
+- Append track's absolute path to selected M3U
+- Provide clear success/failure feedback
+
+** Refactoring for Testability
+
+Following the "Interactive vs Non-Interactive Function Pattern" from =quality-engineer.org=:
+
+*Problem:* Directly implementing as an interactive function would require:
+- Mocking =completing-read=
+- Mocking =emms-playlist-track-at=
+- Testing Emacs UI functionality, not our business logic
+
+*Solution:* Split into two functions:
+
+1. *Helper Function* (=cj/music--append-track-to-m3u-file=):
+ - Pure, deterministic
+ - Takes explicit parameters: =(track-path m3u-file)=
+ - No user interaction
+ - Returns =t= on success, signals errors naturally
+ - 100% testable with ERT, no mocking needed
+
+2. *Interactive Wrapper* (=cj/music-append-track-to-playlist=):
+ - Thin layer handling only user interaction
+ - Gets track at point
+ - Shows completing-read
+ - Catches errors and displays messages
+ - Delegates all business logic to helper
+ - No tests needed (just testing Emacs)
+
+** Benefits of This Pattern
+
+From =quality-engineer.org=:
+#+begin_quote
+When writing functions that combine business logic with user interaction:
+- Split into internal implementation and interactive wrapper
+- Internal function (prefix with =--=): Pure logic, takes all parameters explicitly
+- Dramatically simpler testing (no interactive mocking)
+- Code reusable programmatically without prompts
+- Clear separation of concerns (logic vs UI)
+#+end_quote
+
+This pattern enabled:
+- Zero mocking in tests
+- Fast, deterministic tests
+- Easy reasoning about correctness
+- Reusable helper function
+
+* Phase 2: Writing the First Test
+
+** Test File: =test-music-config--append-track-to-m3u-file.el=
+
+Following the naming convention from =quality-engineer.org=:
+- Pattern: =test-<module>-<function>.el=
+- One test file per function for easy discovery when tests fail
+- User sees failure → immediately knows which file to open
+
+** Test Organization
+
+Following the three-category structure:
+
+*** Normal Cases (4 tests)
+- Append to empty file
+- Append to file with trailing newline
+- Append to file without trailing newline (adds leading newline)
+- Multiple appends (allows duplicates)
+
+*** Boundary Cases (4 tests)
+- Very long paths (~500 chars)
+- Unicode characters (中文, emoji)
+- Spaces and special characters
+- M3U with comments/metadata
+
+*** Error Cases (3 tests)
+- Nonexistent file
+- Read-only file
+- Directory instead of file
+
+** Writing Tests with Zero Mocking
+
+Key principle: "Don't mock what you're testing" (from =quality-engineer.org=)
+
+Example test:
+#+begin_src elisp
+(ert-deftest test-music-config--append-track-to-m3u-file-normal-empty-file-appends-track ()
+ "Append to brand new empty M3U file."
+ (test-music-config--append-track-to-m3u-file-setup)
+ (unwind-protect
+ (let* ((m3u-file (cj/create-temp-test-file "test-playlist-"))
+ (track-path "/home/user/music/artist/song.mp3"))
+ (cj/music--append-track-to-m3u-file track-path m3u-file)
+ (with-temp-buffer
+ (insert-file-contents m3u-file)
+ (should (string= (buffer-string) (concat track-path "\n")))))
+ (test-music-config--append-track-to-m3u-file-teardown)))
+#+end_src
+
+Notice:
+- No mocks
+- Real file I/O using =testutil-general.el= helpers
+- Tests actual function behavior
+- Clean setup/teardown
+
+** Result
+
+All 11 tests passed on first run. The pure, deterministic helper function worked correctly.
+
+* Phase 3: Systematic Test Coverage Analysis
+
+** Identifying Testable Functions
+
+Reviewed all functions in =music-config.el= and categorized:
+
+*** Easy to Test (Pure/Deterministic)
+- =cj/music--valid-file-p= - Extension validation
+- =cj/music--valid-directory-p= - Directory validation
+- =cj/music--safe-filename= - String sanitization
+- =cj/music--m3u-file-tracks= - M3U file parsing
+- =cj/music--get-m3u-basenames= - Basename extraction
+
+*** Medium Complexity (Need File I/O)
+- =cj/music--collect-entries-recursive= - Recursive directory traversal
+- =cj/music--get-m3u-files= - File discovery
+- =cj/music--completion-table= - Completion table generation
+
+*** Hard to Test (EMMS Buffer Dependencies)
+- =cj/music--ensure-playlist-buffer= - EMMS buffer creation
+- =cj/music--playlist-tracks= - EMMS buffer reading
+- =cj/music--playlist-modified-p= - EMMS buffer state
+- =cj/music--assert-valid-playlist-file= - Buffer-local state
+
+*Decision:* Test easy and medium complexity functions. Skip EMMS-dependent functions (would require extensive mocking/setup, diminishing returns).
+
+** File Organization Principle
+
+From =quality-engineer.org=:
+#+begin_quote
+*Unit Tests*: One file per method
+- Naming: =test-<filename>-<methodname>.el=
+- Example: =test-org-gcal--safe-substring.el=
+#+end_quote
+
+*Rationale:* When a test fails in CI:
+1. Developer sees: =test-music-config--get-m3u-files-normal-multiple-files-returns-list FAILED=
+2. Immediately knows: Look for =test-music-config--get-m3u-files.el=
+3. Opens file and fixes issue - *fast cognitive path*
+
+If combined files:
+1. Test fails: =test-music-config--get-m3u-files-normal-multiple-files-returns-list FAILED=
+2. Which file? =test-music-config--m3u-helpers.el=? =test-music-config--combined.el=?
+3. Developer wastes time searching - *slower, frustrating*
+
+*The 1:1 mapping is a usability feature for developers under pressure.*
+
+* Phase 4: Testing Function by Function
+
+** Function 1: =cj/music--valid-file-p=
+
+*** Test Categories
+
+*Normal Cases:*
+- Valid extensions (mp3, flac, etc.)
+- Case-insensitive matching (MP3, Mp3)
+
+*Boundary Cases:*
+- Dots in path (only last extension matters)
+- Multiple extensions (uses rightmost)
+- No extension
+- Empty string
+
+*Error Cases:*
+- Nil input
+- Non-music extensions
+
+*** First Run: 14/15 Passed, 1 FAILED
+
+*Failure:*
+#+begin_src
+test-music-config--valid-file-p-error-nil-input-returns-nil
+Expected: Returns nil gracefully
+Actual: (wrong-type-argument stringp nil) - CRASHED
+#+end_src
+
+*** Bug Analysis: Test or Production Code?
+
+*Process:*
+1. Read the test expectation: "nil input returns nil gracefully"
+2. Read the production code:
+ #+begin_src elisp
+ (defun cj/music--valid-file-p (file)
+ (when-let ((ext (file-name-extension file))) ; ← Crashes here
+ (member (downcase ext) cj/music-file-extensions)))
+ #+end_src
+3. Identify issue: =file-name-extension= expects string, crashes on nil
+4. Consider context: This is defensive validation code, called in various contexts
+
+*Decision: Fix production code*
+
+*Rationale:*
+- Function should be defensive (validation code)
+- Returning nil for invalid input is more robust than crashing
+- Common pattern in Emacs Lisp validation functions
+
+*Fix:*
+#+begin_src elisp
+(defun cj/music--valid-file-p (file)
+ (when (and file (stringp file)) ; ← Guard added
+ (when-let ((ext (file-name-extension file)))
+ (member (downcase ext) cj/music-file-extensions))))
+#+end_src
+
+Result: All 15 tests passed.
+
+** Function 2: =cj/music--valid-directory-p=
+
+*** First Run: 11/13 Passed, 2 FAILED
+
+*Failures:*
+1. Nil input crashed (same pattern as =valid-file-p=)
+2. Empty string returned non-nil (treated as current directory)
+
+*Fix:*
+#+begin_src elisp
+(defun cj/music--valid-directory-p (dir)
+ (when (and dir (stringp dir) (not (string-empty-p dir))) ; ← Guards added
+ (and (file-directory-p dir)
+ (not (string-prefix-p "." (file-name-nondirectory
+ (directory-file-name dir)))))))
+#+end_src
+
+Result: All 13 tests passed.
+
+** Function 3: =cj/music--safe-filename=
+
+*** First Run: 12/13 Passed, 1 FAILED
+
+*Failure:*
+#+begin_src
+test-music-config--safe-filename-boundary-special-chars-replaced
+Expected: "playlist__________" (10 underscores)
+Actual: "playlist_________" (9 underscores)
+#+end_src
+
+*** Bug Analysis: Test or Production Code?
+
+*Process:*
+1. Count special chars in input: =@#$%^&*()= = 9 characters
+2. Test expected 10, but input only has 9
+3. Production code is correct
+
+*Decision: Fix test code*
+
+*The bug was in the test expectation, not the implementation.*
+
+Result: All 13 tests passed.
+
+** Function 4: =cj/music--m3u-file-tracks= (M3U Parser)
+
+This is where we found a **significant bug** through testing!
+
+*** Test Categories
+
+*Normal Cases:*
+- Absolute paths
+- Relative paths (expanded to M3U directory)
+- HTTP/HTTPS/MMS URLs preserved
+- Mixed paths and URLs
+
+*Boundary Cases:*
+- Empty lines ignored (important for playlist robustness!)
+- Whitespace-only lines ignored
+- Comments ignored (#EXTM3U, #EXTINF)
+- Leading/trailing whitespace trimmed
+- Order preserved
+
+*Error Cases:*
+- Nonexistent file
+- Nil input
+
+*** First Run: 11/15 Passed, 4 FAILED
+
+All 4 failures related to URL handling:
+
+*Failure Pattern:*
+#+begin_src
+Expected: "http://example.com/stream.mp3"
+Actual: "/home/cjennings/.temp-emacs-tests/http:/example.com/stream.mp3"
+#+end_src
+
+HTTP/HTTPS/MMS URLs were being treated as relative paths and mangled!
+
+*** Root Cause Analysis
+
+*Production code (line 110):*
+#+begin_src elisp
+(string-match-p "\`\(https?\|mms\)://" line)
+#+end_src
+
+*Problem:* Regex escaping is wrong!
+
+In the string literal ="\`"=:
+- The backslash-backtick becomes a *literal backtick character*
+- Not the regex anchor =\`= (start of string)
+
+The regex never matched, so URLs were treated as relative paths.
+
+*Correct version:*
+#+begin_src elisp
+(string-match-p "\\`\\(https?\\|mms\\)://" line)
+#+end_src
+
+Double backslashes for string literal escaping → results in regex =\`\(https?\|mms\)://=
+
+*** Impact Assessment
+
+*This is a significant bug:*
+- Radio stations (HTTP streams) would be broken
+- Any M3U with URLs would fail
+- Data corruption: URLs transformed into nonsensical file paths
+- Function worked for local files, so bug went unnoticed
+- Users would see mysterious errors when loading playlists with streams
+
+*Tests caught a real production bug that could have caused user data corruption!*
+
+Result: All 15 tests passed after fix.
+
+* Phase 5: Continuing Through the Test Suite
+
+** Functions Tested Successfully
+
+5. =cj/music--get-m3u-files= - 7 tests
+ - Learned: Directory listing order is filesystem-dependent
+ - Solution: Sort results before comparing in tests
+
+6. =cj/music--get-m3u-basenames= - 6 tests
+ - Kept as separate file (not combined with get-m3u-files)
+ - Reason: Usability when tests fail
+
+7. =cj/music--collect-entries-recursive= - 12 tests
+ - Medium complexity: Required creating test directory trees
+ - Used =testutil-general.el= helpers for setup/teardown
+ - All tests passed first time (well-factored function)
+
+8. =cj/music--completion-table= - 12 tests
+ - Tested higher-order function (returns lambda)
+ - Initially misunderstood completion protocol behavior
+ - Fixed test expectations to match actual Emacs behavior
+
+* Key Principles Applied
+
+** 1. Refactor for Testability BEFORE Writing Tests
+
+The Interactive vs Non-Interactive pattern from =quality-engineer.org= made testing trivial:
+- No mocking required
+- Fast, deterministic tests
+- Clear separation of concerns
+
+** 2. Systematic Test Organization
+
+Every test file followed the same structure:
+- Normal Cases
+- Boundary Cases
+- Error Cases
+
+This makes it easy to:
+- Identify coverage gaps
+- Add new tests
+- Understand what's being tested
+
+** 3. Test Naming Convention
+
+Pattern: =test-<module>-<function>-<category>-<scenario>-<expected-result>=
+
+Examples:
+- =test-music-config--valid-file-p-normal-mp3-extension-returns-true=
+- =test-music-config--m3u-file-tracks-boundary-empty-lines-ignored=
+- =test-music-config--safe-filename-error-nil-input-signals-error=
+
+Benefits:
+- Self-documenting
+- Easy to understand what failed
+- Searchable/grepable
+- Clear category organization
+
+** 4. Zero Mocking for Pure Functions
+
+From =quality-engineer.org=:
+#+begin_quote
+DON'T MOCK WHAT YOU'RE TESTING
+- Only mock external side-effects and dependencies, not the domain logic itself
+- If mocking removes the actual work the function performs, you're testing the mock
+- Use real data structures that the function is designed to operate on
+- Rule of thumb: If the function body could be =(error "not implemented")= and tests still pass, you've over-mocked
+#+end_quote
+
+Our tests used:
+- Real file I/O
+- Real strings
+- Real data structures
+- Actual function behavior
+
+Result: Tests caught real bugs, not mock configuration issues.
+
+** 5. Test vs Production Code Bug Decision Framework
+
+When a test fails, ask:
+
+1. *What is the test expecting?*
+ - Read the test name and assertions
+ - Understand the intended behavior
+
+2. *What is the production code doing?*
+ - Read the implementation
+ - Trace through the logic
+
+3. *Which is correct?*
+ - Is the test expectation reasonable?
+ - Is the production behavior defensive/robust?
+ - What is the usage context?
+
+4. *Consider the impact:*
+ - Defensive code: Fix production to handle edge cases
+ - Wrong expectation: Fix test
+ - Unclear spec: Ask user for clarification
+
+Examples from our session:
+- *Nil input crashes* → Fix production (defensive coding)
+- *Empty string treated as valid* → Fix production (defensive coding)
+- *Wrong count in test* → Fix test (test bug)
+- *Regex escaping wrong* → Fix production (real bug!)
+
+** 6. Fast Feedback Loop
+
+Pattern: "Write tests, run them all, report errors, and see where we are!"
+
+This became a mantra during the session:
+1. Write comprehensive tests for one function
+2. Run immediately
+3. Analyze failures
+4. Fix bugs (test or production)
+5. Verify all tests pass
+6. Move to next function
+
+Benefits:
+- Caught bugs immediately
+- Small iteration cycles
+- Clear progress
+- High confidence in changes
+
+* Final Results
+
+** Test Coverage
+
+*9 functions tested, 103 tests total:*
+1. =cj/music--append-track-to-m3u-file= - 11 tests
+2. =cj/music--valid-file-p= - 15 tests
+3. =cj/music--valid-directory-p= - 13 tests
+4. =cj/music--safe-filename= - 13 tests
+5. =cj/music--m3u-file-tracks= - 15 tests
+6. =cj/music--get-m3u-files= - 7 tests
+7. =cj/music--get-m3u-basenames= - 6 tests
+8. =cj/music--collect-entries-recursive= - 12 tests
+9. =cj/music--completion-table= - 12 tests
+
+** Bugs Discovered and Fixed
+
+1. *=cj/music--valid-file-p=*
+ - Issue: Crashed on nil input
+ - Fix: Added nil/string guard
+ - Impact: Prevents crashes in validation code
+
+2. *=cj/music--valid-directory-p=*
+ - Issue: Crashed on nil, treated empty string as valid
+ - Fix: Added guards for nil and empty string
+ - Impact: More robust directory validation
+
+3. *=cj/music--m3u-file-tracks=* ⚠️ *SIGNIFICANT BUG*
+ - Issue: URL regex escaping wrong - HTTP/HTTPS/MMS URLs mangled as relative paths
+ - Fix: Corrected regex escaping: ="\`"= → ="\\`"=
+ - Impact: Radio stations and streaming URLs now work correctly
+ - *This bug would have corrupted user data and broken streaming playlists*
+
+** Code Quality Improvements
+
+- All testable helper functions now have comprehensive test coverage
+- More defensive error handling (nil guards)
+- Clear separation of concerns (pure helpers vs interactive wrappers)
+- Systematic boundary condition testing
+- Unicode and special character handling verified
+
+* Lessons Learned
+
+** 1. Tests as Bug Discovery Tools
+
+Tests aren't just for preventing regressions - they actively *discover existing bugs*:
+- The URL regex bug existed in production
+- Nil handling bugs would have manifested in edge cases
+- Tests made these issues visible immediately
+
+** 2. Refactoring Enables Testing
+
+The decision to split functions into pure helpers + interactive wrappers:
+- Made testing dramatically simpler
+- Enabled 100+ tests with zero mocking
+- Improved code reusability
+- Clarified function responsibilities
+
+** 3. Systematic Process Matters
+
+Following the same pattern for each function:
+- Reduced cognitive load
+- Made it easy to maintain consistency
+- Enabled quick iteration
+- Built confidence in coverage
+
+** 4. File Organization Aids Debugging
+
+One test file per function:
+- Fast discovery when tests fail
+- Clear ownership
+- Easy to maintain
+- Follows user's mental model
+
+** 5. Test Quality Equals Production Quality
+
+Our tests:
+- Used real I/O (not mocks)
+- Tested actual behavior
+- Covered edge cases systematically
+- Found real bugs
+
+This is only possible with well-factored, testable code.
+
+* Applying These Principles
+
+When adding tests to other modules:
+
+1. *Identify testable functions* - Look for pure helpers, file I/O, string manipulation
+2. *Refactor if needed* - Split interactive functions into pure helpers
+3. *Write systematically* - Normal, Boundary, Error categories
+4. *Run frequently* - Fast feedback loop
+5. *Analyze failures carefully* - Test bug vs production bug
+6. *Fix immediately* - Don't accumulate technical debt
+7. *Maintain organization* - One file per function, clear naming
+
+* Reference
+
+See =ai-prompts/quality-engineer.org= for comprehensive quality engineering guidelines, including:
+- Test organization and structure
+- Test naming conventions
+- Mocking and stubbing best practices
+- Interactive vs non-interactive function patterns
+- Integration testing guidelines
+- Test maintenance strategies
+
+Note: =quality-engineer.org= evolves as we learn more quality best practices. This document captures principles applied during this specific session.
+
+* Conclusion
+
+This session demonstrated how systematic testing combined with refactoring for testability can:
+- Discover real bugs before they reach users
+- Improve code quality and robustness
+- Build confidence in changes
+- Create maintainable test suites
+- Follow industry best practices
+
+The 103 tests and 3 bug fixes represent a significant quality improvement to =music-config.el=. The URL regex bug alone justified the entire testing effort - that bug could have caused user data corruption and broken a major feature (streaming radio).
+
+*Testing is not just about preventing future bugs - it's about finding bugs that already exist.*