Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8c3dd7c
chore: pre-commit
Oct 17, 2025
f4384cb
feat: add Mac support through ps filtering; fix mac tests
Oct 22, 2025
191476b
fix: try skipping coverage environment with ci
Oct 24, 2025
9b625e9
Merge branch 'main' into fix_mac
CodyCBakerPhD Oct 24, 2025
53d8343
rf: PR suggestions
Oct 27, 2025
e3f4e2f
chore: fix ci-only (not pre-commit) typing check?
Oct 27, 2025
9e5fa75
pr suggestions
Oct 27, 2025
713684d
release CI locks
Oct 27, 2025
9af908a
only use apt-get on ubuntu
asmacdo Oct 27, 2025
ecd8466
improve logging for incorrect sample types
asmacdo Oct 27, 2025
f341ea9
add print for CI details
Oct 27, 2025
ea0cf46
Merge branch 'fix_mac' of https://github.com/codycbakerphd/duct into …
Oct 27, 2025
5330812
ci: try longer sleep
Oct 27, 2025
2a1a98b
relax print
Oct 27, 2025
eff9079
pin numpy<2.0 for pypy
asmacdo Oct 28, 2025
dbe5d82
Update src/con_duct/__main__.py
CodyCBakerPhD Oct 29, 2025
b59412b
chore: linting
Oct 29, 2025
8344d4c
try list comprehensions and sid prefetching for speed
Oct 29, 2025
0e8ab3d
TMP: debug macos-15-intel spawn children tests
asmacdo Oct 29, 2025
1c3f3ee
TMP: increase spawn_children time 10x
asmacdo Oct 29, 2025
d9408d9
Add -s to tox run on github so we see logged errors (hopefully)
yarikoptic Oct 31, 2025
8bdd44d
Extend log msg with all values
yarikoptic Oct 31, 2025
6b7bdab
Add ERROR log which lead to an empty sample
yarikoptic Oct 31, 2025
02de3a6
fix: try adding retries
Nov 5, 2025
82f679a
chore: remove intel runners
Nov 5, 2025
b473bfd
add longer delay in test; mark OS in config, skip annotation
Nov 5, 2025
8ea87e5
add longer delay in test; still trying to fix linting
Nov 5, 2025
dd67caa
remove temporary code; still fixing linting
Nov 5, 2025
725c4fa
still fixing linting
Nov 5, 2025
2e00734
add warning for intel macOS
Nov 9, 2025
0d8370b
Merge branch 'con:main' into fix_mac
CodyCBakerPhD Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
fail-fast: false
matrix:
os:
# - macos-12
# - macos-latest
- macos-15-intel
- macos-latest
# - windows-latest
- ubuntu-latest
python-version:
Expand Down Expand Up @@ -65,7 +65,9 @@ jobs:
run: |
python -m pip install --upgrade pip wheel
python -m pip install --upgrade --upgrade-strategy=eager tox
sudo apt-get install -y libjpeg-dev

- name: Install system dependencies
run: sudo apt-get install -y libjpeg-dev

- name: Run tests with coverage
if: matrix.toxenv == 'py'
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ build-backend = "setuptools.build_meta"

[tool.versioningit.write]
file = "src/con_duct/_version.py"

[tool.pytest.ini_options]
markers = [
"flaky: mark a test as being unreliable"
]
127 changes: 93 additions & 34 deletions src/con_duct/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import math
import os
import platform
import re
import shutil
import signal
Expand All @@ -22,13 +23,14 @@
import threading
import time
from types import FrameType
from typing import IO, Any, Optional, TextIO
from typing import IO, Any, Callable, Optional, TextIO

__version__ = version("con-duct")
__schema_version__ = "0.2.2"


lgr = logging.getLogger("con-duct")
SYSTEM = platform.system()
DEFAULT_LOG_LEVEL = os.environ.get("DUCT_LOG_LEVEL", "INFO").upper()

DUCT_OUTPUT_PREFIX = os.getenv(
Expand Down Expand Up @@ -359,6 +361,93 @@ def for_json(self) -> dict[str, Any]:
return d


def _get_sample_linux(session_id: int) -> Sample:
sample = Sample()

ps_command = [
"ps",
"-w",
"-s",
str(session_id),
"-o",
"pid,pcpu,pmem,rss,vsz,etime,stat,cmd",
]
output = subprocess.check_output(ps_command, text=True)

for line in output.splitlines()[1:]:
if not line:
continue

pid, pcpu, pmem, rss_kib, vsz_kib, etime, stat, cmd = line.split(maxsplit=7)

sample.add_pid(
pid=int(pid),
stats=ProcessStats(
pcpu=float(pcpu),
pmem=float(pmem),
rss=int(rss_kib) * 1024,
vsz=int(vsz_kib) * 1024,
timestamp=datetime.now().astimezone().isoformat(),
etime=etime,
stat=Counter([stat]),
cmd=cmd,
),
)
sample.averages = Averages.from_sample(sample=sample)
return sample


def _get_sample_mac(session_id: int) -> Sample:
sample = Sample()

ps_command = [
"ps",
"-ax",
"-o",
"pid,pcpu,pmem,rss,vsz,etime,stat,args",
]
output = subprocess.check_output(ps_command, text=True)

for line in output.splitlines()[1:]:
if not line:
continue

pid, pcpu, pmem, rss_kb, vsz_kb, etime, stat, cmd = line.split(maxsplit=7)

try:
sess = os.getsid(int(pid))
except Exception as exc:
lgr.debug(f"Error fetching session ID for PID {pid}: {str(exc)}")
sess = -1

if sess != session_id:
continue

sample.add_pid(
pid=int(pid),
stats=ProcessStats(
pcpu=float(pcpu),
pmem=float(pmem),
rss=int(rss_kb) * 1024,
vsz=int(vsz_kb) * 1024,
timestamp=datetime.now().astimezone().isoformat(),
etime=etime,
stat=Counter([stat]),
cmd=cmd,
),
)

sample.averages = Averages.from_sample(sample=sample)
return sample


_get_sample_per_system = {
"Linux": _get_sample_linux,
"Darwin": _get_sample_mac,
}
_get_sample: Callable[[int], Sample] = _get_sample_per_system[SYSTEM]


class Report:
"""Top level report"""

Expand Down Expand Up @@ -469,44 +558,14 @@ def get_system_info(self) -> None:

def collect_sample(self) -> Optional[Sample]:
assert self.session_id is not None
sample = Sample()

try:
output = subprocess.check_output(
[
"ps",
"-w",
"-s",
str(self.session_id),
"-o",
"pid,pcpu,pmem,rss,vsz,etime,stat,cmd",
],
text=True,
)
for line in output.splitlines()[1:]:
if line:
pid, pcpu, pmem, rss_kib, vsz_kib, etime, stat, cmd = line.split(
maxsplit=7,
)
sample.add_pid(
int(pid),
ProcessStats(
pcpu=float(pcpu),
pmem=float(pmem),
rss=int(rss_kib) * 1024,
vsz=int(vsz_kib) * 1024,
timestamp=datetime.now().astimezone().isoformat(),
etime=etime,
stat=Counter([stat]),
cmd=cmd,
),
)
sample = _get_sample(session_id=self.session_id)
return sample
except subprocess.CalledProcessError as exc: # when session_id has no processes
lgr.debug("Error collecting sample: %s", str(exc))
return None

sample.averages = Averages.from_sample(sample)
return sample

def update_from_sample(self, sample: Sample) -> None:
self.full_run_stats = self.full_run_stats.aggregate(sample)
if self.current_sample is None:
Expand Down
11 changes: 10 additions & 1 deletion test/test_arg_parsing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import platform
import re
import subprocess
from unittest import mock
Expand Down Expand Up @@ -27,6 +28,10 @@ def test_con_duct_version() -> None:
assert re.match(r"con-duct \d+\.\d+\.\d+", output_str)


@pytest.mark.skipif(
condition=platform.system() != "Linux",
reason="Test is specific to Linux platforms.",
)
def test_cmd_help() -> None:
out = subprocess.check_output(["duct", "ps", "--help"])
assert "ps [options]" in str(out)
Expand Down Expand Up @@ -62,7 +67,11 @@ def test_duct_missing_cmd() -> None:
)


def test_abreviation_disabled() -> None:
@pytest.mark.skipif(
condition=platform.system() != "Linux",
reason="Test is specific to Linux platforms.",
)
def test_abbreviation_disabled() -> None:
"""
If abbreviation is enabled, options passed to command (not duct) are still
filtered through the argparse and causes problems.
Expand Down
Loading