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, 0 insertions, 593 deletions
diff --git a/docs/sessions/refactor.org b/docs/sessions/refactor.org
deleted file mode 100644
index 11ff0a91..00000000
--- a/docs/sessions/refactor.org
+++ /dev/null
@@ -1,593 +0,0 @@
-#+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.*