-
Notifications
You must be signed in to change notification settings - Fork 206
feat: add --related* flags to run affected tests only
#765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
stephanschubert
wants to merge
4
commits into
lunarmodules:master
Choose a base branch
from
stephanschubert:feat/related-tests
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
fb4bde0
feat(related): add --related flag for running affected tests
stephanschubert fa08601
test(related): add unit and integration tests
stephanschubert d28962b
docs: document --related flag usage in README
stephanschubert 0f1b28f
refactor(related): improve error handling and test consistency
stephanschubert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| local RequireParser = require 'busted.modules.related.require_parser' | ||
|
|
||
| local DependencyGraph = {} | ||
| DependencyGraph.__index = DependencyGraph | ||
|
|
||
| function DependencyGraph.new() | ||
| local self = setmetatable({}, DependencyGraph) | ||
| self.forward = {} | ||
| self.reverse = {} | ||
| self.module_to_path = {} | ||
| return self | ||
| end | ||
|
|
||
| function DependencyGraph:build(files, path_resolver, options) | ||
| options = options or {} | ||
| local verbose = options.verbose | ||
|
|
||
| for _, filepath in ipairs(files) do | ||
| local parsed, err = RequireParser.parse_file(filepath) | ||
| if not parsed and verbose then | ||
| io.stderr:write('Warning: Failed to parse ' .. filepath .. ': ' .. (err or 'unknown error') .. '\n') | ||
| end | ||
| if parsed then | ||
| self.forward[filepath] = {} | ||
|
|
||
| for _, module_name in ipairs(parsed.requires) do | ||
| local resolved = self.module_to_path[module_name] | ||
| if not resolved then | ||
| resolved = path_resolver:resolve(module_name) | ||
| self.module_to_path[module_name] = resolved | ||
| end | ||
|
|
||
| if resolved then | ||
| self.forward[filepath][#self.forward[filepath] + 1] = resolved | ||
| end | ||
| end | ||
|
|
||
| for _, file_path in ipairs(parsed.loadfiles) do | ||
| local resolved = path_resolver:resolve_file(file_path) | ||
| if resolved then | ||
| self.forward[filepath][#self.forward[filepath] + 1] = resolved | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| for filepath, deps in pairs(self.forward) do | ||
| for _, dep in ipairs(deps) do | ||
| if not self.reverse[dep] then | ||
| self.reverse[dep] = {} | ||
| end | ||
| self.reverse[dep][#self.reverse[dep] + 1] = filepath | ||
| end | ||
| end | ||
| end | ||
|
|
||
| function DependencyGraph:get_direct_dependents(filepath) | ||
| return self.reverse[filepath] or {} | ||
| end | ||
|
|
||
| function DependencyGraph:get_direct_dependencies(filepath) | ||
| return self.forward[filepath] or {} | ||
| end | ||
|
|
||
| function DependencyGraph:get_affected_files(changed_files) | ||
| local affected = {} | ||
| local visited = {} | ||
| local queue = {} | ||
|
|
||
| for _, filepath in ipairs(changed_files) do | ||
| queue[#queue + 1] = filepath | ||
| end | ||
|
|
||
| local queue_start = 1 | ||
| while queue_start <= #queue do | ||
| local current = queue[queue_start] | ||
| queue_start = queue_start + 1 | ||
|
|
||
| if not visited[current] then | ||
| visited[current] = true | ||
| affected[current] = true | ||
|
|
||
| local dependents = self.reverse[current] or {} | ||
| for _, dependent in ipairs(dependents) do | ||
| if not visited[dependent] then | ||
| queue[#queue + 1] = dependent | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| return affected | ||
| end | ||
|
|
||
| function DependencyGraph:get_affected_tests(changed_files, test_files) | ||
| local affected = self:get_affected_files(changed_files) | ||
| local affected_tests = {} | ||
|
|
||
| for filepath in pairs(affected) do | ||
| if test_files[filepath] then | ||
| affected_tests[filepath] = true | ||
| end | ||
| end | ||
|
|
||
| return affected_tests | ||
| end | ||
|
|
||
| function DependencyGraph:stats() | ||
| local num_files = 0 | ||
| local num_edges = 0 | ||
|
|
||
| for _, deps in pairs(self.forward) do | ||
| num_files = num_files + 1 | ||
| num_edges = num_edges + #deps | ||
| end | ||
|
|
||
| return { | ||
| files = num_files, | ||
| edges = num_edges, | ||
| } | ||
| end | ||
|
|
||
| function DependencyGraph:dump() | ||
| print("=== Forward edges (file -> dependencies) ===") | ||
| for filepath, deps in pairs(self.forward) do | ||
| print(filepath) | ||
| for _, dep in ipairs(deps) do | ||
| print(" -> " .. dep) | ||
| end | ||
| end | ||
|
|
||
| print("\n=== Reverse edges (file -> dependents) ===") | ||
| for filepath, deps in pairs(self.reverse) do | ||
| print(filepath) | ||
| for _, dep in ipairs(deps) do | ||
| print(" <- " .. dep) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| return DependencyGraph |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| local path = require 'pl.path' | ||
|
|
||
| local GitChanges = {} | ||
|
|
||
| -- Normalize paths to forward slashes for cross-platform consistency | ||
| local function normalize(p) | ||
| return p:gsub('\\', '/') | ||
| end | ||
|
|
||
| local function default_run_command(cmd, cwd) | ||
| local full_cmd | ||
| if cwd then | ||
| full_cmd = string.format('cd %q && %s 2>&1', cwd, cmd) | ||
| else | ||
| full_cmd = cmd .. ' 2>&1' | ||
| end | ||
|
|
||
| local handle = io.popen(full_cmd) | ||
| if not handle then | ||
| return nil, 'Failed to execute command: ' .. cmd | ||
| end | ||
|
|
||
| local lines = {} | ||
| local has_error = false | ||
| for line in handle:lines() do | ||
| if line:match('^fatal:') or line:match('^error:') then | ||
| has_error = true | ||
| end | ||
| lines[#lines + 1] = line | ||
| end | ||
|
|
||
| local _, _, exit_code = handle:close() | ||
|
|
||
| if has_error or (exit_code and exit_code ~= 0) then | ||
| local err_msg = table.concat(lines, '\n') | ||
| return nil, err_msg ~= '' and err_msg or ('Git command failed: ' .. cmd) | ||
| end | ||
|
|
||
| return lines | ||
| end | ||
|
|
||
| -- Injectable for testing | ||
| GitChanges._run_command = default_run_command | ||
|
|
||
| local function run_git_command(cmd, cwd) | ||
| return GitChanges._run_command(cmd, cwd) | ||
| end | ||
|
|
||
| function GitChanges.is_git_repo(cwd) | ||
| local lines = run_git_command('git rev-parse --is-inside-work-tree', cwd) | ||
| return lines and #lines > 0 and lines[1] == 'true' | ||
| end | ||
|
|
||
| function GitChanges.get_git_root(cwd) | ||
| local lines = run_git_command('git rev-parse --show-toplevel', cwd) | ||
| return lines and lines[1] | ||
| end | ||
|
|
||
| function GitChanges.get_changed_files(cwd) | ||
| if not GitChanges.is_git_repo(cwd) then | ||
| return nil, 'Not a git repository' | ||
| end | ||
|
|
||
| local files = {} | ||
|
|
||
| local unstaged = run_git_command('git diff --name-only', cwd) | ||
| if unstaged then | ||
| for _, file in ipairs(unstaged) do | ||
| files[normalize(path.normpath(path.join(cwd, file)))] = true | ||
| end | ||
| end | ||
|
|
||
| local staged = run_git_command('git diff --cached --name-only', cwd) | ||
| if staged then | ||
| for _, file in ipairs(staged) do | ||
| files[normalize(path.normpath(path.join(cwd, file)))] = true | ||
| end | ||
| end | ||
|
|
||
| local untracked = run_git_command('git ls-files --others --exclude-standard', cwd) | ||
| if untracked then | ||
| for _, file in ipairs(untracked) do | ||
| files[normalize(path.normpath(path.join(cwd, file)))] = true | ||
| end | ||
| end | ||
|
|
||
| local result = {} | ||
| for file in pairs(files) do | ||
| result[#result + 1] = file | ||
| end | ||
| table.sort(result) | ||
|
|
||
| return result | ||
| end | ||
|
|
||
| function GitChanges.get_changes_since(cwd, base_ref) | ||
| if not GitChanges.is_git_repo(cwd) then | ||
| return nil, 'Not a git repository' | ||
| end | ||
|
|
||
| local files = {} | ||
|
|
||
| local cmd = string.format('git diff --name-only %q', base_ref) | ||
| local diff = run_git_command(cmd, cwd) | ||
| if diff then | ||
| for _, file in ipairs(diff) do | ||
| files[normalize(path.normpath(path.join(cwd, file)))] = true | ||
| end | ||
| end | ||
|
|
||
| local uncommitted = GitChanges.get_changed_files(cwd) | ||
| if uncommitted then | ||
| for _, file in ipairs(uncommitted) do | ||
| files[file] = true | ||
| end | ||
| end | ||
|
|
||
| local result = {} | ||
| for file in pairs(files) do | ||
| result[#result + 1] = file | ||
| end | ||
| table.sort(result) | ||
|
|
||
| return result | ||
| end | ||
|
|
||
| function GitChanges.filter_lua_files(files) | ||
| local result = {} | ||
| for _, file in ipairs(files) do | ||
| if file:match('%.lua$') then | ||
| result[#result + 1] = file | ||
| end | ||
| end | ||
| return result | ||
| end | ||
|
|
||
| return GitChanges |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functionality directly baked into Busted should never assume Git is the VCS in charge.