Skip to content

Commit 2c93d47

Browse files
committed
Add new self test framework (for 9pm.py)
This is a totally new 9pm.py self test suite. Its only purpose is to ensure 9pm.py works as expected. The old unit_tests tests both tcl and the harness together. The goal now is to separate the tcl code from the python3 harness. This means we need standalone tests for 9pm.py. Hence this commit. Signed-off-by: Richard Alpe <[email protected]>
1 parent 4681991 commit 2c93d47

File tree

13 files changed

+389
-0
lines changed

13 files changed

+389
-0
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
gem install --user-install rouge
2828
echo "PATH=$PATH:$(ruby -e 'puts Gem.user_dir')/bin" >> $GITHUB_ENV
2929
30+
- name: Run Self Tests
31+
run: python3 self_test/run.py
32+
3033
- name: Run Unit Tests
3134
run: python3 9pm.py --option cmdl-supplied unit_tests/auto.yaml
3235

9pm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def rootify_path(path):
5959
return path
6060

6161
def execute(args, test, output_log):
62+
os.environ["NINEPM_TEST_NAME"] = test['name']
6263
proc = subprocess.Popen([test['case']] + args, stdout=subprocess.PIPE)
6364
skip_suite = False
6465
test_skip = False
@@ -741,6 +742,8 @@ def main():
741742
VERBOSE = args.verbose
742743
NOEXEC = args.no_exec
743744

745+
vcprint(pcolor.faint, f"Verbose output turned on")
746+
744747
rc = parse_rc(ROOT_PATH, args)
745748

746749
LOGDIR = setup_log_dir(rc['LOG_PATH'])

self_test/cases/fail.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
3+
echo "1..1"
4+
echo "not ok 1 - Dummy test fail"

self_test/cases/pass.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
3+
echo "1..1"
4+
echo "ok 1 - Dummy test pass"

self_test/cases/worker.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import os
5+
import json
6+
7+
test_dir = os.environ.get("NINEPM_TEST_DIR") or sys.exit(
8+
"Fatal: NINEPM_TEST_DIR is not set."
9+
)
10+
test_name = os.environ.get("NINEPM_TEST_NAME") or sys.exit(
11+
"Fatal: NINEPM_TEST_NAME is not set."
12+
)
13+
14+
15+
test_name = test_name.rsplit(".", 1)[0]
16+
17+
log_path = os.path.join(test_dir, f"{test_name}.log")
18+
print(f'Hello from "{test_name}"')
19+
print(f'Log to: "{log_path}"')
20+
21+
data = {
22+
"name": test_name,
23+
"args": sys.argv[1:],
24+
}
25+
if "NINEPM_DEBUG" in os.environ:
26+
data["debug"] = os.environ["NINEPM_DEBUG"]
27+
if "NINEPM_CONFIG" in os.environ:
28+
data["config"] = os.environ["NINEPM_CONFIG"]
29+
30+
with open(log_path, "w") as f:
31+
json.dump(data, f)
32+
33+
print(f"Wrote data to log path")
34+
print("1..1")
35+
print(f"ok 1 - All done ({test_name})")

self_test/cases/worker1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
worker.py

self_test/run.py

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/usr/bin/env python3
2+
3+
import subprocess
4+
import os
5+
import json
6+
import sys
7+
import tempfile
8+
import argparse
9+
import uuid
10+
11+
12+
VERBOSE = False
13+
14+
15+
# ANSI color codes
16+
def print_green(msg):
17+
"""Prints a message in green (pass)."""
18+
print(f"\033[92m{msg}\033[0m")
19+
20+
21+
def print_red(msg):
22+
"""Prints a message in red (fail)."""
23+
print(f"\033[91m{msg}\033[0m")
24+
25+
26+
def print_cyan(msg):
27+
"""Prints a message in yellow (info/warning)."""
28+
print(f"\033[96m{msg}\033[0m")
29+
30+
31+
class Test9pm:
32+
def __init__(self, ninepm="../9pm.py"):
33+
self.ninepm = ninepm
34+
self.script_dir = os.path.dirname(os.path.abspath(__file__))
35+
self.temp_dir_base = (
36+
tempfile.TemporaryDirectory()
37+
) # Creates a temporary directory
38+
self.env = os.environ.copy()
39+
self.env["NINEPM_TEST_DIR"] = self.temp_dir_base.name
40+
41+
def create_unique_subdir(self):
42+
subdir = os.path.join(self.temp_dir_base.name, str(uuid.uuid4()))
43+
os.makedirs(subdir)
44+
return subdir
45+
46+
def run(self, workers, args=None, expected_return=0, grep=None):
47+
"""Run 9pm.py with a worker script and arguments, ensuring correct order.
48+
If 'grep' is provided, it ensures that the specified text appears in the output.
49+
"""
50+
args = args or [] # Default to empty list if no args are provided
51+
command = ["python3", self.ninepm, "-v"] + workers + args
52+
53+
if VERBOSE:
54+
print_cyan(f"Executing {command}")
55+
56+
result = subprocess.run(
57+
command,
58+
cwd=self.script_dir,
59+
text=True,
60+
env=self.env,
61+
stdout=subprocess.PIPE,
62+
stderr=subprocess.PIPE,
63+
)
64+
65+
if VERBOSE:
66+
print(result.stdout)
67+
print(result.stderr, file=sys.stderr)
68+
69+
# Ensure the return code is as expected
70+
assert result.returncode == expected_return, f"Failed: {result.stderr}"
71+
72+
# If grep is provided, ensure the output contains the specified text
73+
if grep:
74+
output = result.stdout + result.stderr # Combine both streams for searching
75+
assert grep in output, f"Expected text '{grep}' not found in output!"
76+
77+
def check(self, expected, descr):
78+
"""Check if the worker script wrote the expected JSON result and print colored results."""
79+
file_name = f"{expected['name']}.log"
80+
file_path = os.path.join(self.env["NINEPM_TEST_DIR"], file_name)
81+
82+
if not os.path.exists(file_path):
83+
print_red(
84+
f"[FAIL] {descr} - Missing output file: {file_name} {file_path} (name error?)"
85+
)
86+
assert False, f"Missing output file: {file_name}"
87+
88+
with open(file_path, "r") as f:
89+
data = json.load(f)
90+
91+
if data != expected:
92+
print_red(f"[FAIL] {descr} - Unexpected content in {file_name}")
93+
print_cyan("Expected:")
94+
print(json.dumps(expected, indent=4))
95+
print_cyan("Got:")
96+
print(json.dumps(data, indent=4))
97+
assert False, f"Unexpected content in {file_name}"
98+
99+
def test(self, test):
100+
self.env = os.environ.copy()
101+
self.env["NINEPM_TEST_DIR"] = self.create_unique_subdir()
102+
# print(f"Test will write to {self.env["NINEPM_TEST_DIR"]}")
103+
104+
self.run(test["tests"], test["args"])
105+
106+
for expected in test["expected"]:
107+
self.check(expected, test["desc"])
108+
109+
print_green(f"[PASS] {test['desc']} ({len(test['expected'])} tests)")
110+
111+
def test_suites(self):
112+
"""Verify various suite setups"""
113+
114+
self.test(
115+
{
116+
"desc": "Basic suite",
117+
"args": [],
118+
"tests": ["suites/suite.yaml"],
119+
"expected": [
120+
{"name": "0002-worker", "args": []},
121+
{"name": "0003-worker", "args": []},
122+
{"name": "0004-worker1", "args": []},
123+
],
124+
}
125+
)
126+
127+
self.test(
128+
{
129+
"desc": "Test case naming",
130+
"args": [],
131+
"tests": ["suites/names.yaml"],
132+
"expected": [
133+
{"name": "0002-my-worker", "args": []},
134+
{"name": "0003-worker", "args": []},
135+
{"name": "0004-my-worker1", "args": []},
136+
],
137+
}
138+
)
139+
self.test(
140+
{
141+
"desc": "Test case options",
142+
"args": ["-o", "cmdline"],
143+
"tests": ["suites/options.yaml"],
144+
"expected": [
145+
{"name": "0002-worker", "args": ["cmdline"]},
146+
{"name": "0003-worker", "args": ["opt1", "opt2", "cmdline"]},
147+
],
148+
}
149+
)
150+
151+
self.test(
152+
{
153+
"desc": "Nested test case options",
154+
"args": ["-o", "cmdline"],
155+
"tests": ["suites/top-options.yaml"],
156+
"expected": [
157+
{"name": "0002-worker", "args": ["top1", "cmdline"]},
158+
{"name": "0004-worker", "args": ["top2", "cmdline"]},
159+
{
160+
"name": "0005-worker",
161+
"args": ["opt1", "opt2", "top2", "cmdline"],
162+
},
163+
],
164+
}
165+
)
166+
167+
self.test(
168+
{
169+
"desc": "Nested suites",
170+
"args": [],
171+
"tests": ["suites/top-suite.yaml"],
172+
"expected": [
173+
{"name": "0002-worker", "args": []},
174+
{"name": "0004-worker", "args": []},
175+
{"name": "0006-worker", "args": []},
176+
{"name": "0007-worker", "args": []},
177+
{"name": "0008-worker1", "args": []},
178+
{"name": "0009-worker1", "args": []},
179+
{"name": "0010-worker1", "args": []},
180+
],
181+
}
182+
)
183+
184+
def test_cmdline_options(self):
185+
"""Verify that command line options (-o) are passed to test(s)"""
186+
187+
self.test(
188+
{
189+
"desc": "No argument",
190+
"args": [],
191+
"tests": ["cases/worker.py"],
192+
"expected": [{"name": "0001-worker", "args": []}],
193+
}
194+
)
195+
196+
self.test(
197+
{
198+
"desc": "One option (cmdline argument)",
199+
"args": ["-o", "foobar"],
200+
"tests": ["cases/worker.py"],
201+
"expected": [{"name": "0001-worker", "args": ["foobar"]}],
202+
}
203+
)
204+
205+
self.test(
206+
{
207+
"desc": "Multiple options (cmdline argument)",
208+
"args": ["-o", "foo", "-o", "bar", "-o", "baz"],
209+
"tests": ["cases/worker.py"],
210+
"expected": [{"name": "0001-worker", "args": ["foo", "bar", "baz"]}],
211+
}
212+
)
213+
214+
self.test(
215+
{
216+
"desc": "Two tests, no argument",
217+
"args": [],
218+
"tests": ["cases/worker.py", "cases/worker.py"],
219+
"expected": [
220+
{"name": "0001-worker", "args": []},
221+
{"name": "0002-worker", "args": []},
222+
],
223+
}
224+
)
225+
226+
self.test(
227+
{
228+
"desc": "Two tests, multiple arguments",
229+
"args": ["-o", "foo", "-o", "bar", "-o", "baz"],
230+
"tests": ["cases/worker.py", "cases/worker.py"],
231+
"expected": [
232+
{"name": "0001-worker", "args": ["foo", "bar", "baz"]},
233+
{"name": "0002-worker", "args": ["foo", "bar", "baz"]},
234+
],
235+
}
236+
)
237+
238+
def test_debug_flag(self):
239+
"""Verify that the debug flag accessible from tests"""
240+
241+
self.test(
242+
{
243+
"desc": "Pass debug flag to test cases",
244+
"args": ["-d"],
245+
"tests": ["cases/worker.py", "cases/worker.py"],
246+
"expected": [
247+
{"name": "0001-worker", "args": [], "debug": "1"},
248+
{"name": "0002-worker", "args": [], "debug": "1"},
249+
],
250+
}
251+
)
252+
253+
def test_config_file(self):
254+
"""Verify that the config file is passed to all tests"""
255+
256+
self.test(
257+
{
258+
"desc": "Pass config file (-c) to test cases",
259+
"args": ["-c", "conf.txt"],
260+
"tests": ["cases/worker.py", "cases/worker.py"],
261+
"expected": [
262+
{"name": "0001-worker", "args": [], "config": "conf.txt"},
263+
{"name": "0002-worker", "args": [], "config": "conf.txt"},
264+
],
265+
}
266+
)
267+
268+
def test_verbose_flag(self):
269+
"""Verify that that 9pm verbose output works (-v)"""
270+
271+
self.run(["cases/worker.py", "cases/worker.py"], ["-v"], 0, "Verbose output turned on")
272+
273+
def test_abort_flag(self):
274+
"""Verify that that 9pm abort flag works (--abort)"""
275+
276+
self.run(["cases/fail.sh", "cases/pass.sh"], ["--abort"], 1, "Aborting execution")
277+
278+
def cleanup(self):
279+
"""Cleanup temp directory after tests"""
280+
self.temp_dir_base.cleanup()
281+
282+
283+
# Command-line argument parsing
284+
if __name__ == "__main__":
285+
parser = argparse.ArgumentParser(description="Run 9pm.py tests.")
286+
parser.add_argument(
287+
"-v",
288+
"--verbose",
289+
action="store_true",
290+
help="Enable verbose output (show 9pm.py stdout/stderr).",
291+
)
292+
293+
args = parser.parse_args()
294+
VERBOSE = args.verbose
295+
296+
tester = Test9pm()
297+
298+
try:
299+
tester.test_suites()
300+
tester.test_cmdline_options()
301+
tester.test_debug_flag()
302+
tester.test_config_file()
303+
tester.test_verbose_flag()
304+
tester.test_abort_flag()
305+
print_green("All tests passed.")
306+
finally:
307+
tester.cleanup()

self_test/suites/middle-suite.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
- case: "../cases/worker.py"
3+
- suite: "suite.yaml"
4+
- case: "../cases/worker1.py"

self_test/suites/names.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
- case: "../cases/worker.py"
3+
name: "my-worker"
4+
- case: "../cases/worker.py"
5+
- case: "../cases/worker1.py"
6+
name: "my-worker1"

0 commit comments

Comments
 (0)