1+ import importlib .util
2+ import json
3+ import os
4+ import shutil
5+ import subprocess
6+ import sys
7+ import tempfile
8+ import unittest
9+
10+ __DIR__ = os .path .dirname (os .path .abspath (__file__ ))
11+ __PYTHONENV__ = '/../.venv/bin/python'
12+
13+ # Preload src/lib so backend_utils is resolvable when importing lifecycle module
14+ sys .path .insert (0 , os .path .join (__DIR__ , '..' , 'src' , 'lib' ))
15+
16+ _spec = importlib .util .spec_from_file_location (
17+ 'lifecycle_bin' ,
18+ os .path .join (__DIR__ , '..' , 'src' , 'bin' , 'lifecycle.py' ),
19+ )
20+ _lc = importlib .util .module_from_spec (_spec )
21+ _spec .loader .exec_module (_lc )
22+
23+
24+ class TestFindScript (unittest .TestCase ):
25+
26+ def setUp (self ):
27+ self .tmpdir = tempfile .mkdtemp ()
28+
29+ def tearDown (self ):
30+ shutil .rmtree (self .tmpdir )
31+
32+ def _touch (self , name ):
33+ path = os .path .join (self .tmpdir , name )
34+ open (path , 'w' ).close ()
35+ return path
36+
37+ def test_finds_py_extension (self ):
38+ self ._touch ('lifecycle.py' )
39+ self .assertEqual (
40+ _lc .find_script (self .tmpdir , ['lifecycle' ]),
41+ os .path .join (self .tmpdir , 'lifecycle.py' ),
42+ )
43+
44+ def test_finds_sh_extension (self ):
45+ self ._touch ('O_I.sh' )
46+ self .assertEqual (
47+ _lc .find_script (self .tmpdir , ['O_I' ]),
48+ os .path .join (self .tmpdir , 'O_I.sh' ),
49+ )
50+
51+ def test_finds_pl_extension (self ):
52+ self ._touch ('lifecycle.pl' )
53+ self .assertEqual (
54+ _lc .find_script (self .tmpdir , ['lifecycle' ]),
55+ os .path .join (self .tmpdir , 'lifecycle.pl' ),
56+ )
57+
58+ def test_finds_no_extension (self ):
59+ self ._touch ('lifecycle' )
60+ self .assertEqual (
61+ _lc .find_script (self .tmpdir , ['lifecycle' ]),
62+ os .path .join (self .tmpdir , 'lifecycle' ),
63+ )
64+
65+ def test_specific_name_preferred_over_fallback (self ):
66+ self ._touch ('O_I.py' )
67+ self ._touch ('lifecycle.py' )
68+ self .assertEqual (
69+ _lc .find_script (self .tmpdir , ['O_I' , 'lifecycle' ]),
70+ os .path .join (self .tmpdir , 'O_I.py' ),
71+ )
72+
73+ def test_falls_back_to_second_name (self ):
74+ self ._touch ('lifecycle.py' )
75+ self .assertEqual (
76+ _lc .find_script (self .tmpdir , ['O_I' , 'lifecycle' ]),
77+ os .path .join (self .tmpdir , 'lifecycle.py' ),
78+ )
79+
80+ def test_returns_none_when_not_found (self ):
81+ self .assertIsNone (_lc .find_script (self .tmpdir , ['nonexistent' ]))
82+
83+ def test_returns_none_on_empty_dir (self ):
84+ self .assertIsNone (_lc .find_script (self .tmpdir , ['lifecycle' ]))
85+
86+ def test_returns_none_on_empty_names (self ):
87+ self ._touch ('lifecycle.py' )
88+ self .assertIsNone (_lc .find_script (self .tmpdir , []))
89+
90+
91+ class TestRunBackend (unittest .TestCase ):
92+
93+ def setUp (self ):
94+ self ._scripts = []
95+
96+ def tearDown (self ):
97+ for s in self ._scripts :
98+ try :
99+ os .unlink (s )
100+ except OSError :
101+ pass
102+
103+ def _make_script (self , body ):
104+ fd , path = tempfile .mkstemp (suffix = '.py' , prefix = 'lc_test_' )
105+ os .write (fd , ('#!/usr/bin/python3\n ' + body ).encode ())
106+ os .close (fd )
107+ os .chmod (path , 0o755 )
108+ self ._scripts .append (path )
109+ return path
110+
111+ def test_returncode_zero (self ):
112+ script = self ._make_script ('print("ok")\n ' )
113+ self .assertEqual (_lc .run_backend (script , '' )['returncode' ], 0 )
114+
115+ def test_returncode_nonzero (self ):
116+ script = self ._make_script ('import sys\n sys.exit(3)\n ' )
117+ self .assertEqual (_lc .run_backend (script , '' )['returncode' ], 3 )
118+
119+ def test_content_passed_via_stdin (self ):
120+ script = self ._make_script ('import sys\n print(sys.stdin.read().strip())\n ' )
121+ ret = _lc .run_backend (script , 'ping' )
122+ self .assertEqual (ret ['returncode' ], 0 )
123+ self .assertIn ('ping' , ret ['stdout' ])
124+
125+ def test_stdout_captured_as_string (self ):
126+ script = self ._make_script ('print("hello lifecycle")\n ' )
127+ ret = _lc .run_backend (script , '' )
128+ self .assertIsInstance (ret ['stdout' ], str )
129+ self .assertIn ('hello lifecycle' , ret ['stdout' ])
130+
131+
132+ class TestLifecycleBin (unittest .TestCase ):
133+ """Integration tests: invoke lifecycle.py as a subprocess."""
134+
135+ def run_backend (self , script , file ):
136+ dir_ = os .getcwd ()
137+ exe = dir_ + __PYTHONENV__
138+ with open (file , 'r' ) as fic :
139+ content = fic .read ().replace ('\n ' , '' )
140+ os .chdir ('../src/bin' )
141+ ret = subprocess .run ([exe , script ], input = content .encode (), capture_output = True )
142+ os .chdir (dir_ )
143+ return {'returncode' : ret .returncode , 'stdout' : ret .stdout .decode ()}
144+
145+ def test_lifecycle_concerned (self ):
146+ """lifecycle.json: O→I transition, concerned backend → lifecycle script runs."""
147+ ret = self .run_backend ('lifecycle.py' , './files_ad_utils/lifecycle.json' )
148+ self .assertEqual (ret ['returncode' ], 0 )
149+ result = json .loads (ret ['stdout' ])
150+ self .assertEqual (result ['status' ], 0 )
151+ self .assertEqual (result ['message' ], 'lifecycle.py' )
152+
153+ def test_lifecycle_notconcerned (self ):
154+ """lifecycle-notconcerned.json: backend not concerned → not concerned."""
155+ ret = self .run_backend ('lifecycle.py' , './files_ad_utils/lifecycle-notconcerned.json' )
156+ self .assertEqual (ret ['returncode' ], 0 )
157+ result = json .loads (ret ['stdout' ])
158+ self .assertEqual (result ['status' ], 0 )
159+ self .assertEqual (result ['message' ], 'not concerned' )
160+
161+
162+ if __name__ == '__main__' :
163+ unittest .main ()
0 commit comments