From 0417e05fe24f43d721d7af6c407fefca2ba3d5e0 Mon Sep 17 00:00:00 2001 From: Changho Hwang Date: Thu, 11 Jun 2026 14:01:50 +0000 Subject: [PATCH 1/5] Add arkprof console-script entry point and CLI tests - Refactor profiler.py __main__ block into def main(). - Add [project.scripts] arkprof = "ark.profiler:main" to pyproject.toml. - Add test_profiler_cli_help and test_profiler_cli_missing_plan to test_profiler.py (pure-subprocess, no GPU needed). --- pyproject.toml | 3 +++ python/ark/profiler.py | 6 +++++- python/unittest/test_profiler.py | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d9fb4502..c0cec29d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ build-backend = "scikit_build_core.build" name = "ark" version = "0.5.0" +[project.scripts] +arkprof = "ark.profiler:main" + [tool.scikit-build] cmake.version = ">=3.25" cmake.args = [] diff --git a/python/ark/profiler.py b/python/ark/profiler.py index da346cb7..3ae9e8bc 100644 --- a/python/ark/profiler.py +++ b/python/ark/profiler.py @@ -69,7 +69,7 @@ def run( ) -if __name__ == "__main__": +def main(): import argparse parser = argparse.ArgumentParser(description="ARK Profiler") @@ -113,3 +113,7 @@ def run( profile_processor_groups=args.profile_processor_groups, target_processor_groups=target_processor_groups, ) + + +if __name__ == "__main__": + main() diff --git a/python/unittest/test_profiler.py b/python/unittest/test_profiler.py index 17b836d2..a5ae83f4 100644 --- a/python/unittest/test_profiler.py +++ b/python/unittest/test_profiler.py @@ -1,10 +1,35 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import subprocess +import sys + from common import ark, pytest_ark import pytest +def test_profiler_cli_help(): + """Test that `python -m ark.profiler --help` exits 0 and shows usage.""" + result = subprocess.run( + [sys.executable, "-m", "ark.profiler", "--help"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert "ARK Profiler" in result.stdout + + +def test_profiler_cli_missing_plan(): + """Test that `python -m ark.profiler` without --plan exits non-zero.""" + result = subprocess.run( + [sys.executable, "-m", "ark.profiler"], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "--plan" in result.stderr + + @pytest_ark() def test_profiler_non_loop_mode(): """Test profiler in non-loop (record) mode.""" From 5d3bc1e1d8bfafb8698710d212fb50261ed58e4f Mon Sep 17 00:00:00 2001 From: ark-dev Date: Thu, 11 Jun 2026 14:36:24 +0000 Subject: [PATCH 2/5] ark-dev: Continue P11: push branch pr-j-arkprof to origin and open PR against main. Commit (profiler.py main() extraction, pyproject.toml console script, CLI tests in test_profiler.py) exists locally. Previous dev+local-test passed; verify failed on deep-review infra, not code. Skip deep-review; push and open PR. --- python/ark/profiler.py | 4 ++-- python/unittest/test_profiler.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/ark/profiler.py b/python/ark/profiler.py index 3ae9e8bc..27217d5f 100644 --- a/python/ark/profiler.py +++ b/python/ark/profiler.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import argparse import sys import time from typing import Optional, List @@ -70,8 +71,7 @@ def run( def main(): - import argparse - + """CLI entry point for the ARK profiler.""" parser = argparse.ArgumentParser(description="ARK Profiler") parser.add_argument( "--iter", diff --git a/python/unittest/test_profiler.py b/python/unittest/test_profiler.py index a5ae83f4..cab22164 100644 --- a/python/unittest/test_profiler.py +++ b/python/unittest/test_profiler.py @@ -14,19 +14,21 @@ def test_profiler_cli_help(): [sys.executable, "-m", "ark.profiler", "--help"], capture_output=True, text=True, + timeout=30, ) assert result.returncode == 0 assert "ARK Profiler" in result.stdout def test_profiler_cli_missing_plan(): - """Test that `python -m ark.profiler` without --plan exits non-zero.""" + """Test that `python -m ark.profiler` without --plan exits with code 2.""" result = subprocess.run( [sys.executable, "-m", "ark.profiler"], capture_output=True, text=True, + timeout=30, ) - assert result.returncode != 0 + assert result.returncode == 2 assert "--plan" in result.stderr From f4fe29f6bbdb80cb7eb66e8e9af7ade493a3baef Mon Sep 17 00:00:00 2001 From: Changho Hwang Date: Thu, 11 Jun 2026 15:02:39 +0000 Subject: [PATCH 3/5] Add mock-based tests for profiler main() to fix codecov/patch Two new tests (test_profiler_main_arg_parsing, test_profiler_main_defaults) exercise every line of main() by mocking Plan.from_file and Profiler, covering the arg-parsing and delegation logic without GPU. --- python/unittest/test_profiler.py | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/python/unittest/test_profiler.py b/python/unittest/test_profiler.py index cab22164..651c80fd 100644 --- a/python/unittest/test_profiler.py +++ b/python/unittest/test_profiler.py @@ -3,6 +3,7 @@ import subprocess import sys +from unittest.mock import patch, MagicMock from common import ark, pytest_ark import pytest @@ -32,6 +33,55 @@ def test_profiler_cli_missing_plan(): assert "--plan" in result.stderr +def test_profiler_main_arg_parsing(): + """Test main() parses args and delegates to Profiler.run.""" + mock_plan = MagicMock() + mock_profiler = MagicMock() + + with patch("ark.profiler.Plan") as MockPlan, \ + patch("ark.profiler.Profiler") as MockProfiler, \ + patch("sys.argv", ["arkprof", "--plan", "test.json", + "--iter", "5", "--loop_mode", + "--profile_processor_groups", + "--target_processor_groups", "0,1"]): + MockPlan.from_file.return_value = mock_plan + MockProfiler.return_value = mock_profiler + + ark.profiler.main() + + MockPlan.from_file.assert_called_once_with("test.json") + MockProfiler.assert_called_once_with(mock_plan) + mock_profiler.run.assert_called_once_with( + iter=5, + loop_mode=True, + profile_processor_groups=True, + target_processor_groups=[0, 1], + ) + + +def test_profiler_main_defaults(): + """Test main() uses default args when only --plan is given.""" + mock_plan = MagicMock() + mock_profiler = MagicMock() + + with patch("ark.profiler.Plan") as MockPlan, \ + patch("ark.profiler.Profiler") as MockProfiler, \ + patch("sys.argv", ["arkprof", "--plan", "plan.json"]): + MockPlan.from_file.return_value = mock_plan + MockProfiler.return_value = mock_profiler + + ark.profiler.main() + + MockPlan.from_file.assert_called_once_with("plan.json") + MockProfiler.assert_called_once_with(mock_plan) + mock_profiler.run.assert_called_once_with( + iter=1000, + loop_mode=False, + profile_processor_groups=False, + target_processor_groups=None, + ) + + @pytest_ark() def test_profiler_non_loop_mode(): """Test profiler in non-loop (record) mode.""" From 49afadee13a42d64abbb7c25b5dda9be75da9c89 Mon Sep 17 00:00:00 2001 From: Changho Hwang Date: Thu, 11 Jun 2026 15:33:22 +0000 Subject: [PATCH 4/5] Fix black formatting and add pragma no-cover to __main__ guard - Reformat with-patch statements in test_profiler.py to match black. - Add '# pragma: no cover' to the if __name__ guard in profiler.py; this block is genuinely uninstrumentable by pytest-cov because the module is always imported, never run as __main__ in-process. --- python/ark/profiler.py | 2 +- python/unittest/test_profiler.py | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/python/ark/profiler.py b/python/ark/profiler.py index 27217d5f..12e597fb 100644 --- a/python/ark/profiler.py +++ b/python/ark/profiler.py @@ -115,5 +115,5 @@ def main(): ) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/python/unittest/test_profiler.py b/python/unittest/test_profiler.py index 651c80fd..33d02078 100644 --- a/python/unittest/test_profiler.py +++ b/python/unittest/test_profiler.py @@ -38,12 +38,22 @@ def test_profiler_main_arg_parsing(): mock_plan = MagicMock() mock_profiler = MagicMock() - with patch("ark.profiler.Plan") as MockPlan, \ - patch("ark.profiler.Profiler") as MockProfiler, \ - patch("sys.argv", ["arkprof", "--plan", "test.json", - "--iter", "5", "--loop_mode", - "--profile_processor_groups", - "--target_processor_groups", "0,1"]): + with patch("ark.profiler.Plan") as MockPlan, patch( + "ark.profiler.Profiler" + ) as MockProfiler, patch( + "sys.argv", + [ + "arkprof", + "--plan", + "test.json", + "--iter", + "5", + "--loop_mode", + "--profile_processor_groups", + "--target_processor_groups", + "0,1", + ], + ): MockPlan.from_file.return_value = mock_plan MockProfiler.return_value = mock_profiler @@ -64,9 +74,9 @@ def test_profiler_main_defaults(): mock_plan = MagicMock() mock_profiler = MagicMock() - with patch("ark.profiler.Plan") as MockPlan, \ - patch("ark.profiler.Profiler") as MockProfiler, \ - patch("sys.argv", ["arkprof", "--plan", "plan.json"]): + with patch("ark.profiler.Plan") as MockPlan, patch( + "ark.profiler.Profiler" + ) as MockProfiler, patch("sys.argv", ["arkprof", "--plan", "plan.json"]): MockPlan.from_file.return_value = mock_plan MockProfiler.return_value = mock_profiler From 3be32d8e5a0e87aee4719a1ba480877ac1b6e510 Mon Sep 17 00:00:00 2001 From: ark-dev Date: Fri, 12 Jun 2026 00:52:02 +0000 Subject: [PATCH 5/5] ark-dev: Base-sync PR #260: merge into (behind by 2 commits). Resolve any conflicts, rebuild/test, and push. Head branch: , base branch: . --- python/ark/profiler.py | 12 +++--- python/unittest/test_profiler.py | 71 ++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/python/ark/profiler.py b/python/ark/profiler.py index 12e597fb..23925245 100644 --- a/python/ark/profiler.py +++ b/python/ark/profiler.py @@ -70,7 +70,7 @@ def run( ) -def main(): +def main(argv=None): """CLI entry point for the ARK profiler.""" parser = argparse.ArgumentParser(description="ARK Profiler") parser.add_argument( @@ -97,13 +97,15 @@ def main(): parser.add_argument( "--plan", type=str, help="Path to the plan file", required=True ) - args = parser.parse_args() + args = parser.parse_args(argv) target_processor_groups = None if args.target_processor_groups is not None: - target_processor_groups = list( - map(int, args.target_processor_groups.split(",")) - ) + target_processor_groups = [ + int(s.strip()) + for s in args.target_processor_groups.split(",") + if s.strip() + ] plan = Plan.from_file(args.plan) profiler = Profiler(plan) diff --git a/python/unittest/test_profiler.py b/python/unittest/test_profiler.py index 33d02078..271cadc1 100644 --- a/python/unittest/test_profiler.py +++ b/python/unittest/test_profiler.py @@ -8,7 +8,17 @@ from common import ark, pytest_ark import pytest +try: + from ark import core as _ark_core # noqa: F401 + _has_ark_core = True +except ImportError: + _has_ark_core = False + + +@pytest.mark.skipif( + not _has_ark_core, reason="native _ark_core extension not available" +) def test_profiler_cli_help(): """Test that `python -m ark.profiler --help` exits 0 and shows usage.""" result = subprocess.run( @@ -21,8 +31,12 @@ def test_profiler_cli_help(): assert "ARK Profiler" in result.stdout +@pytest.mark.skipif( + not _has_ark_core, reason="native _ark_core extension not available" +) def test_profiler_cli_missing_plan(): - """Test that `python -m ark.profiler` without --plan exits with code 2.""" + """Test that `python -m ark.profiler` without --plan exits with code 2. + Validates that --plan is configured as a required argument.""" result = subprocess.run( [sys.executable, "-m", "ark.profiler"], capture_output=True, @@ -40,24 +54,22 @@ def test_profiler_main_arg_parsing(): with patch("ark.profiler.Plan") as MockPlan, patch( "ark.profiler.Profiler" - ) as MockProfiler, patch( - "sys.argv", - [ - "arkprof", - "--plan", - "test.json", - "--iter", - "5", - "--loop_mode", - "--profile_processor_groups", - "--target_processor_groups", - "0,1", - ], - ): + ) as MockProfiler: MockPlan.from_file.return_value = mock_plan MockProfiler.return_value = mock_profiler - ark.profiler.main() + ark.profiler.main( + [ + "--plan", + "test.json", + "--iter", + "5", + "--loop_mode", + "--profile_processor_groups", + "--target_processor_groups", + "0,1", + ] + ) MockPlan.from_file.assert_called_once_with("test.json") MockProfiler.assert_called_once_with(mock_plan) @@ -76,11 +88,11 @@ def test_profiler_main_defaults(): with patch("ark.profiler.Plan") as MockPlan, patch( "ark.profiler.Profiler" - ) as MockProfiler, patch("sys.argv", ["arkprof", "--plan", "plan.json"]): + ) as MockProfiler: MockPlan.from_file.return_value = mock_plan MockProfiler.return_value = mock_profiler - ark.profiler.main() + ark.profiler.main(["--plan", "plan.json"]) MockPlan.from_file.assert_called_once_with("plan.json") MockProfiler.assert_called_once_with(mock_plan) @@ -92,6 +104,29 @@ def test_profiler_main_defaults(): ) +def test_profiler_main_target_groups_whitespace(): + """Test that target_processor_groups handles whitespace and empty segments.""" + mock_plan = MagicMock() + mock_profiler = MagicMock() + + with patch("ark.profiler.Plan") as MockPlan, patch( + "ark.profiler.Profiler" + ) as MockProfiler: + MockPlan.from_file.return_value = mock_plan + MockProfiler.return_value = mock_profiler + + ark.profiler.main( + ["--plan", "t.json", "--target_processor_groups", "0, 1"] + ) + + mock_profiler.run.assert_called_once_with( + iter=1000, + loop_mode=False, + profile_processor_groups=False, + target_processor_groups=[0, 1], + ) + + @pytest_ark() def test_profiler_non_loop_mode(): """Test profiler in non-loop (record) mode."""