From 60cc9b5632828d9337571d23521c51b833032097 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Mon, 12 Aug 2024 17:08:55 -0500 Subject: [PATCH 01/12] add test script before tests --- test/data/abandoning_parent.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 test/data/abandoning_parent.sh diff --git a/test/data/abandoning_parent.sh b/test/data/abandoning_parent.sh new file mode 100755 index 00000000..03eb5349 --- /dev/null +++ b/test/data/abandoning_parent.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +nchildren=$1 +shift + +for i in `seq 1 $nchildren`; do + "$@" & +done + +echo "Started $nchildren for $$" +pstree -c -p "$$" + +echo "Starting one more in subprocess" + +( "$@" & ) + +jobs + +pstree -c -p "$$" +echo "waiting" +wait From b3ba30b694e64615af902754d788faa32d1d0f45 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Fri, 15 Nov 2024 10:40:30 -0600 Subject: [PATCH 02/12] Add basic implementation of abandoning parent test Just check that we have the correct number of processes Fixes #44 --- test/test_e2e.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/test_e2e.py diff --git a/test/test_e2e.py b/test/test_e2e.py new file mode 100644 index 00000000..a4e4ab84 --- /dev/null +++ b/test/test_e2e.py @@ -0,0 +1,29 @@ +from __future__ import annotations +import json +from pathlib import Path +import subprocess + +ABANDONING_PARENT = str(Path(__file__).with_name("data") / "abandoning_parent.sh") + + +def test_sanity(temp_output_dir: str) -> None: + command = f"duct -p {temp_output_dir}log_ sleep 0.1" + subprocess.check_output(command, shell=True) + + +def test_abandoning_parent(temp_output_dir: str) -> None: + duct_prefix = f"{temp_output_dir}log_" + num_children = 3 + command = f"duct -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" + subprocess.check_output(command, shell=True) + + with open(f"{duct_prefix}usage.json") as usage_file: + all_samples = [json.loads(line) for line in usage_file] + + max_processes_sample = {"processes": {}} + for sample in all_samples: + if len(max_processes_sample) < len(sample.get("processes")): + max_processes_sample = sample + + # 1 for each child, 1 for pstree, 1 for parent + assert len(max_processes_sample) == num_children + 2 From 713152e2c64bcd226cee557ca49b9f8ad4051c41 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Fri, 15 Nov 2024 10:57:31 -0600 Subject: [PATCH 03/12] WIP: Passes, but ONLY when sleep is 0.1, longer periods result in too many PIDs?? --- test/test_e2e.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index a4e4ab84..62496f76 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -2,6 +2,7 @@ import json from pathlib import Path import subprocess +import pytest ABANDONING_PARENT = str(Path(__file__).with_name("data") / "abandoning_parent.sh") @@ -11,9 +12,9 @@ def test_sanity(temp_output_dir: str) -> None: subprocess.check_output(command, shell=True) -def test_abandoning_parent(temp_output_dir: str) -> None: +@pytest.mark.parametrize("num_children", [3, 5, 10, 20]) +def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - num_children = 3 command = f"duct -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" subprocess.check_output(command, shell=True) @@ -25,5 +26,12 @@ def test_abandoning_parent(temp_output_dir: str) -> None: if len(max_processes_sample) < len(sample.get("processes")): max_processes_sample = sample + cmds = [proc["cmd"] for _pid, proc in max_processes_sample["processes"].items()] + + # DEBUG + from pprint import pprint + + pprint(cmds) + # 1 for each child, 1 for pstree, 1 for parent - assert len(max_processes_sample) == num_children + 2 + assert len(max_processes_sample["processes"]) == num_children + 2 From 26653b5b122749bbe203d00e1dd2981fcde145f6 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Tue, 14 Jan 2025 09:48:32 -0600 Subject: [PATCH 04/12] comment out pstree calls Each invocation of pstree adds extra pids, which throws off the pid count --- test/data/abandoning_parent.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/data/abandoning_parent.sh b/test/data/abandoning_parent.sh index 03eb5349..89e79a26 100755 --- a/test/data/abandoning_parent.sh +++ b/test/data/abandoning_parent.sh @@ -8,7 +8,8 @@ for i in `seq 1 $nchildren`; do done echo "Started $nchildren for $$" -pstree -c -p "$$" +# Can be useful when running manually, but commented out so we can count pids in tests +# pstree -c -p "$$" echo "Starting one more in subprocess" @@ -16,6 +17,7 @@ echo "Starting one more in subprocess" jobs -pstree -c -p "$$" +# Can be useful when running manually, but commented out so we can count pids in tests +# pstree -c -p "$$" echo "waiting" wait From d5cd163c336928e1d7137491a376e5f3ad74600d Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Tue, 14 Jan 2025 09:52:19 -0600 Subject: [PATCH 05/12] Add type annotation for max_processes_sample --- test/test_e2e.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index 62496f76..eb917838 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -2,6 +2,7 @@ import json from pathlib import Path import subprocess +from typing import Dict import pytest ABANDONING_PARENT = str(Path(__file__).with_name("data") / "abandoning_parent.sh") @@ -21,7 +22,7 @@ def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: with open(f"{duct_prefix}usage.json") as usage_file: all_samples = [json.loads(line) for line in usage_file] - max_processes_sample = {"processes": {}} + max_processes_sample: Dict[str, Dict] = {"processes": {}} for sample in all_samples: if len(max_processes_sample) < len(sample.get("processes")): max_processes_sample = sample From def3fdc6651b1f58f021b76613eca21de2dd0d97 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Tue, 14 Jan 2025 09:58:37 -0600 Subject: [PATCH 06/12] More samples to ensure all pids are captured --- test/test_e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index eb917838..d524df5a 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -16,7 +16,7 @@ def test_sanity(temp_output_dir: str) -> None: @pytest.mark.parametrize("num_children", [3, 5, 10, 20]) def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - command = f"duct -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" + command = f"duct --s-i 0.01 --r-i 0.02 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" subprocess.check_output(command, shell=True) with open(f"{duct_prefix}usage.json") as usage_file: From e91d72e6be8b6edae3c542b26981a86e9b8ae2bf Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Tue, 14 Jan 2025 11:56:06 -0600 Subject: [PATCH 07/12] dont assume that all processes will be collected in a single sample --- test/test_e2e.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index d524df5a..ff754502 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -2,7 +2,6 @@ import json from pathlib import Path import subprocess -from typing import Dict import pytest ABANDONING_PARENT = str(Path(__file__).with_name("data") / "abandoning_parent.sh") @@ -13,26 +12,19 @@ def test_sanity(temp_output_dir: str) -> None: subprocess.check_output(command, shell=True) -@pytest.mark.parametrize("num_children", [3, 5, 10, 20]) +@pytest.mark.parametrize("num_children", [1, 10, 25, 101]) def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - command = f"duct --s-i 0.01 --r-i 0.02 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" + command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" subprocess.check_output(command, shell=True) with open(f"{duct_prefix}usage.json") as usage_file: all_samples = [json.loads(line) for line in usage_file] - max_processes_sample: Dict[str, Dict] = {"processes": {}} + all_processes = {} for sample in all_samples: - if len(max_processes_sample) < len(sample.get("processes")): - max_processes_sample = sample - - cmds = [proc["cmd"] for _pid, proc in max_processes_sample["processes"].items()] - - # DEBUG - from pprint import pprint - - pprint(cmds) + for pid, proc in sample["processes"].items(): + all_processes[pid] = proc # 1 for each child, 1 for pstree, 1 for parent - assert len(max_processes_sample["processes"]) == num_children + 2 + assert len(all_processes) == num_children + 2 From 43b8bf5b7bdb48bc6f7e09640ff12b5ed297714e Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Tue, 14 Jan 2025 14:56:46 -0600 Subject: [PATCH 08/12] Increase sleep to prevent flake It seems like whats happening here is that if there are sufficiently many child processes to launch, duct does not collect a sample while the PID is still running. So we can increase the sleep time to avoid the problem for now. --- test/test_e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index ff754502..3b3161ab 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -15,7 +15,7 @@ def test_sanity(temp_output_dir: str) -> None: @pytest.mark.parametrize("num_children", [1, 10, 25, 101]) def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.1" + command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.3" subprocess.check_output(command, shell=True) with open(f"{duct_prefix}usage.json") as usage_file: From 7cc272ae2ca9114096d1ecd9f1182331ef01632b Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Wed, 15 Jan 2025 11:42:37 -0600 Subject: [PATCH 09/12] Add a little more time to avoid flake on pypy3.9 --- test/test_e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index 3b3161ab..2f3aaff0 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -15,7 +15,7 @@ def test_sanity(temp_output_dir: str) -> None: @pytest.mark.parametrize("num_children", [1, 10, 25, 101]) def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.3" + command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.35" subprocess.check_output(command, shell=True) with open(f"{duct_prefix}usage.json") as usage_file: From 3568c7958da6b06adcd9302e43eee3bc6956498a Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Wed, 15 Jan 2025 14:49:55 -0600 Subject: [PATCH 10/12] only keep necessary info --- test/test_e2e.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index 2f3aaff0..d139ebda 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -1,4 +1,5 @@ from __future__ import annotations +from itertools import chain import json from pathlib import Path import subprocess @@ -21,10 +22,10 @@ def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: with open(f"{duct_prefix}usage.json") as usage_file: all_samples = [json.loads(line) for line in usage_file] - all_processes = {} - for sample in all_samples: - for pid, proc in sample["processes"].items(): - all_processes[pid] = proc + all_pids = { + pid + for pid in chain.from_iterable(sample["processes"] for sample in all_samples) + } # 1 for each child, 1 for pstree, 1 for parent - assert len(all_processes) == num_children + 2 + assert len(all_pids) == num_children + 2 From 6423200f88fbb35ab994e418e1adc2b752dd24a0 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Wed, 15 Jan 2025 15:26:55 -0600 Subject: [PATCH 11/12] Even dryer Co-authored-by: Yaroslav Halchenko --- test/test_e2e.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index d139ebda..a18258b6 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -22,10 +22,7 @@ def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: with open(f"{duct_prefix}usage.json") as usage_file: all_samples = [json.loads(line) for line in usage_file] - all_pids = { - pid - for pid in chain.from_iterable(sample["processes"] for sample in all_samples) - } + all_pids = set(chain.from_iterable(sample["processes"] for sample in all_samples)) # 1 for each child, 1 for pstree, 1 for parent assert len(all_pids) == num_children + 2 From 9074a961e9f71f3e497029c6c0b5e358b46c457b Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 22 Jan 2025 12:22:07 -0500 Subject: [PATCH 12/12] Make test faster by not bothering with 100s of processes --- test/test_e2e.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_e2e.py b/test/test_e2e.py index a18258b6..7e932aad 100644 --- a/test/test_e2e.py +++ b/test/test_e2e.py @@ -13,10 +13,10 @@ def test_sanity(temp_output_dir: str) -> None: subprocess.check_output(command, shell=True) -@pytest.mark.parametrize("num_children", [1, 10, 25, 101]) +@pytest.mark.parametrize("num_children", [1, 2, 10]) def test_abandoning_parent(temp_output_dir: str, num_children: int) -> None: duct_prefix = f"{temp_output_dir}log_" - command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.35" + command = f"duct --s-i 0.001 --r-i 0.01 -p {duct_prefix} {ABANDONING_PARENT} {num_children} sleep 0.2" subprocess.check_output(command, shell=True) with open(f"{duct_prefix}usage.json") as usage_file: