3434
3535DEFAULT_REPO = "kunitoki/yup"
3636
37- DEFAULT_TARGETS = [
38- "yup_GpuPipeline.cpp" ,
39- "yup_GpuRenderPass.cpp" ,
40- "yup_GpuCanvas.cpp" ,
41- "yup_GpuBuffer.cpp" ,
42- "yup_GpuTexture.cpp" ,
43- "yup_Image.cpp" ,
44- "yup_TypeErasedObject.h" ,
45- "yup_Graphics.cpp" ,
46- "yup_GpuPipelineCache.cpp" ,
47- "yup_GpuFrame.cpp" ,
48- ]
49-
5037
5138def get_github_token () -> str :
5239 """Return the GitHub token from environment variables."""
@@ -183,14 +170,45 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
183170 sys .exit (1 )
184171
185172
173+ def get_pr_changed_files (pr_number : int , repo : str , token : str ) -> set [str ]:
174+ """Return the set of file paths changed in a PR.
175+
176+ Paths are relative to the repository root (e.g.
177+ ``modules/yup_graphics/graphics/yup_GpuPipeline.cpp``).
178+ """
179+ files : set [str ] = set ()
180+ page = 1
181+
182+ while True :
183+ url = (
184+ f"https://api.github.com/repos/{ repo } /pulls/{ pr_number } /files?per_page=100&page={ page } "
185+ )
186+ data = github_api_request (url , token )
187+
188+ if not isinstance (data , list ):
189+ break
190+
191+ for entry in data :
192+ filename = entry .get ("filename" , "" )
193+ if filename :
194+ files .add (filename )
195+
196+ if len (data ) < 100 :
197+ break
198+ page += 1
199+
200+ return files
201+
202+
186203def resolve_coverage_from_pr (
187204 pr_number : int ,
188205 repo : str ,
189206 workflow_name : str ,
190- ) -> Path :
207+ ) -> tuple [ Path , set [ str ]] :
191208 """Fetch the coverage artifact from a GitHub Actions PR run.
192209
193- Returns the path to the extracted ``coverage_final.info`` file.
210+ Returns a ``(coverage_path, changed_files)`` tuple where *changed_files*
211+ is the set of file paths modified in the PR.
194212 """
195213 token = get_github_token ()
196214
@@ -292,7 +310,14 @@ def resolve_coverage_from_pr(
292310
293311 coverage_path = info_files [0 ]
294312 print (f"Using coverage file: { coverage_path } " )
295- return coverage_path
313+
314+ # ------------------------------------------------------------------
315+ # 6. Get the list of files changed in the PR
316+ # ------------------------------------------------------------------
317+ changed_files = get_pr_changed_files (pr_number , repo , token )
318+ print (f"PR #{ pr_number } changed { len (changed_files )} file(s)" )
319+
320+ return coverage_path , changed_files
296321
297322
298323def parse_arguments () -> argparse .Namespace :
@@ -326,7 +351,8 @@ def parse_arguments() -> argparse.Namespace:
326351 parser .add_argument (
327352 "--all" ,
328353 action = "store_true" ,
329- help = "Report every source file found in the coverage file." ,
354+ help = "Report every source file found in the coverage file. "
355+ "With --pr this bypasses both the PR-changed-files and targets filters." ,
330356 )
331357 parser .add_argument (
332358 "--repo" ,
@@ -372,22 +398,47 @@ def parse_lcov_records(content: str) -> list[tuple[str, list[tuple[int, int]]]]:
372398 return records
373399
374400
375- def should_report (source_file : str , targets : list [str ], report_all : bool ) -> bool :
376- return report_all or any (target in source_file for target in targets )
401+ def should_report (
402+ source_file : str ,
403+ targets : list [str ],
404+ report_all : bool ,
405+ changed_files : set [str ] | None = None ,
406+ ) -> bool :
407+ """Return True if *source_file* should appear in the coverage report."""
408+ if report_all :
409+ return True
410+
411+ if changed_files is not None :
412+ # Only report files that were changed in the PR.
413+ # SF paths in LCOV are absolute; PR file paths are repo-relative.
414+ # Match by checking if any PR path is a suffix of the SF path.
415+ if not any (source_file .endswith (f"/{ f } " ) or source_file .endswith (f"\\ { f } " ) for f in changed_files ):
416+ return False
417+
418+ if targets :
419+ return any (target in source_file for target in targets )
420+
421+ # No targets filter active — show everything that passed earlier checks.
422+ return True
377423
378424
379425def display_name (source_file : str , targets : list [str ], report_all : bool ) -> str :
380- if report_all :
426+ if report_all or len ( targets ) == 0 :
381427 return source_file
382428
383- return next (target for target in targets if target in source_file )
429+ return next (( target for target in targets if target in source_file ), source_file )
384430
385431
386- def print_uncovered_lines (coverage_file : Path , targets : list [str ], report_all : bool ) -> None :
432+ def print_uncovered_lines (
433+ coverage_file : Path ,
434+ targets : list [str ],
435+ report_all : bool ,
436+ changed_files : set [str ] | None = None ,
437+ ) -> None :
387438 content = coverage_file .read_text (encoding = "utf-8" )
388439
389440 for source_file , coverage_lines in parse_lcov_records (content ):
390- if not should_report (source_file , targets , report_all ):
441+ if not should_report (source_file , targets , report_all , changed_files ):
391442 continue
392443
393444 hit = [line_number for line_number , count in coverage_lines if count > 0 ]
@@ -404,14 +455,19 @@ def print_uncovered_lines(coverage_file: Path, targets: list[str], report_all: b
404455
405456def main () -> None :
406457 args = parse_arguments ()
407- targets = args . targets if args . targets else DEFAULT_TARGETS
458+ changed_files : set [ str ] | None = None
408459
409460 if args .pr is not None :
410- coverage_file = resolve_coverage_from_pr (args .pr , args .repo , args .workflow )
461+ coverage_file , changed_files = resolve_coverage_from_pr (
462+ args .pr , args .repo , args .workflow
463+ )
411464 else :
412465 coverage_file = args .coverage_file
466+ changed_files = None
467+
468+ targets = args .targets
413469
414- print_uncovered_lines (coverage_file , targets , args .all )
470+ print_uncovered_lines (coverage_file , targets , args .all , changed_files )
415471
416472
417473if __name__ == "__main__" :
0 commit comments