2424from importlib import import_module
2525from pathlib import Path
2626from typing import Iterable , List , Optional
27+ from collections import namedtuple
2728
2829import drgn
2930import sdb
3031from sdb .internal .cli import load_debug_info
3132from sdb .internal .repl import REPL
3233
3334THIS_DIR = os .path .dirname (os .path .realpath (__file__ ))
34- DATA_DIR = f"{ THIS_DIR } /data"
35- DUMP_PATH = f"{ DATA_DIR } /dump.201912060006"
36- MODS_PATH = f"{ DATA_DIR } /mods"
37- VMLX_PATH = f"{ DATA_DIR } /vmlinux-5.0.0-36-generic"
38- TEST_OUTPUT_DIR = f"{ DATA_DIR } /regression_output"
39-
40-
41- def dump_exists () -> bool :
42- """
43- Used as the sole indicator of whether the integration
44- tests will run.
45- """
46- return os .path .exists (DUMP_PATH ) and os .path .exists (
47- MODS_PATH ) and os .path .exists (VMLX_PATH )
48-
49-
50- def setup_target () -> Optional [drgn .Program ]:
51- """
52- Create a drgn.Program instance and setup the SDB
53- context for all the integration tests. If there
54- is no crash dump to attach to this is going to
55- be an empty drgn.Program.
56- """
57- prog = drgn .Program ()
58- if not dump_exists ():
59- return prog
60- prog .set_core_dump (DUMP_PATH )
61- load_debug_info (prog , [VMLX_PATH , MODS_PATH ])
62- return prog
63-
64-
65- TEST_PROGRAM = setup_target ()
66- TEST_REPL = REPL (TEST_PROGRAM , list (sdb .get_registered_commands ().keys ()))
67-
68-
69- def repl_invoke (cmd : str ) -> int :
70- """
71- Accepts a command/pipeline in string form and evaluates
72- it returning the exit code of the evaluation emulating
73- the SDB repl.
74- """
75- assert TEST_PROGRAM
76- return TEST_REPL .eval_cmd (cmd )
77-
78-
79- def sdb_invoke (objs : Iterable [drgn .Object ], line : str ) -> Iterable [drgn .Object ]:
80- """
81- Dispatch to sdb.invoke, but also drain the generator it returns, so
82- the tests can more easily access the returned objects.
83-
84- This method is preferred over repl_invoke() when the test wants to
85- do fancier checks by mocking a few objects that are later passed
86- down to the pipeline. Other scenarios include but are not limited
87- to testing that specific exceptions are thrown or analyzing internal
88- state of objects that is not part of the output in stdout.
89- """
90- assert TEST_PROGRAM
91- return list (sdb .invoke (TEST_PROGRAM , objs , line ))
92-
93-
94- def slurp_output_file (modname : str , cmd : str ) -> str :
95- """
96- Given a module name and a command, find the output file
97- and return all of its contents as a string.
98- """
99- return Path (f"{ TEST_OUTPUT_DIR } /{ modname } /{ cmd } " ).read_text ()
100-
101-
102- def generate_output_for_commands (cmds : List [str ], dirpath : str ) -> None :
103- """
104- Takes a list of SDB commands in string form, invokes them in the
105- context of the current crash dump/sdb.REPL, and stores their output
106- in the directory specified, each under a different file.
107-
108- Note: Keep in mind that if the directory specified exists then
109- it will be removed together with all of its contents.
110- """
111- assert TEST_PROGRAM
112- if os .path .exists (dirpath ):
113- shutil .rmtree (dirpath )
114- os .makedirs (dirpath )
115- for cmd in cmds :
116- with open (f"{ dirpath } /{ cmd } " , 'w' ) as f :
117- with redirect_stdout (f ):
118- repl_invoke (cmd )
119-
120-
121- def generate_output_for_test_module (modname : str ) -> None :
122- """
123- Generates the regression output for all the commands of
124- a test module given module name. The assumption for this
125- to work automatically is that for the given modname "mod"
126- there exist a test module under test.integration named
127- test_mod_generic which has a list of commands in string
128- form called CMD_TABLE.
129- """
130- test_mod = import_module (f"tests.integration.test_{ modname } _generic" )
131- generate_output_for_commands (
132- test_mod .CMD_TABLE , # type: ignore[attr-defined]
133- f"{ TEST_OUTPUT_DIR } /{ modname } " )
134- print (f"Generated regression test output for { modname } ..." )
35+ PRIMARY = "primary"
36+ ALTERNATE = "alternate"
13537
13638
39+ @staticmethod
13740def get_all_generic_test_modules () -> List [str ]:
13841 """
13942 Look at this current directory and capture all modules
@@ -148,10 +51,127 @@ def get_all_generic_test_modules() -> List[str]:
14851 return modnames
14952
15053
151- def generate_known_regression_output () -> None :
152- """
153- Auto-generate the baseline regression output for all
154- the detected test modules in this directory.
155- """
156- for modname in get_all_generic_test_modules ():
157- generate_output_for_test_module (modname )
54+ class Infra :
55+ """
56+ Encapsulate crash dump management for automated tests.
57+ """
58+ Crashdump_Record = namedtuple (
59+ 'Crashdump_Record' , 'data_dir, dump_path, \
60+ mods_path, vmlx_path, regression_directory' )
61+ cdr_primary = Crashdump_Record (f"{ THIS_DIR } /data" ,
62+ f"{ THIS_DIR } /data/dump.201912060006" ,
63+ f"{ THIS_DIR } /data/mods" ,
64+ f"{ THIS_DIR } /data/vmlinux-5.0.0-36-generic" ,
65+ f"{ THIS_DIR } /data/regression_output" )
66+ cdr_alternate = Crashdump_Record (
67+ f"{ THIS_DIR } /data_alternate" ,
68+ f"{ THIS_DIR } /data_alternate/dump.202102031354" ,
69+ f"{ THIS_DIR } /data_alternate/mods" ,
70+ f"{ THIS_DIR } /data_alternate/vmlinux-5.8.0-41-generic" ,
71+ f"{ THIS_DIR } /data_alternate/regression_output" )
72+
73+ def __init__ (self , which_dump : str ):
74+ assert which_dump in (PRIMARY , ALTERNATE )
75+ if which_dump == PRIMARY :
76+ self .cdr = self .cdr_primary
77+ else :
78+ self .cdr = self .cdr_alternate
79+
80+ def dump_exists (self ) -> bool :
81+ """
82+ Used as the sole indicator of whether the integration
83+ tests will run.
84+ """
85+ return os .path .exists (self .cdr .dump_path ) and os .path .exists (
86+ self .cdr .mods_path ) and os .path .exists (self .cdr .vmlx_path )
87+
88+ def setup_target (self ) -> Optional [drgn .Program ]:
89+ """
90+ Create a drgn.Program instance and setup the SDB
91+ context for all the integration tests. If there
92+ is no crash dump to attach to this is going to
93+ be an empty drgn.Program.
94+ """
95+ prog = drgn .Program ()
96+ if not self .dump_exists ():
97+ return prog
98+ prog .set_core_dump (self .cdr .dump_path )
99+ load_debug_info (prog , [self .cdr .vmlx_path , self .cdr .mods_path ])
100+ return prog
101+
102+ def repl_invoke (self , cmd : str ) -> int :
103+ """
104+ Accepts a command/pipeline in string form and evaluates
105+ it returning the exit code of the evaluation emulating
106+ the SDB repl.
107+ """
108+ prog = self .setup_target ()
109+ assert prog
110+ return REPL (prog ,
111+ list (sdb .get_registered_commands ().keys ())).eval_cmd (cmd )
112+
113+ def sdb_invoke (self , objs : Iterable [drgn .Object ],
114+ line : str ) -> Iterable [drgn .Object ]:
115+ """
116+ Dispatch to sdb.invoke, but also drain the generator it returns, so
117+ the tests can more easily access the returned objects.
118+
119+ This method is preferred over repl_invoke() when the test wants to
120+ do fancier checks by mocking a few objects that are later passed
121+ down to the pipeline. Other scenarios include but are not limited
122+ to testing that specific exceptions are thrown or analyzing internal
123+ state of objects that is not part of the output in stdout.
124+ """
125+ prog = self .setup_target ()
126+ assert prog
127+ return list (sdb .invoke (prog , objs , line ))
128+
129+ def slurp_output_file (self , modname : str , cmd : str ) -> str :
130+ """
131+ Given a module name and a command, find the output file
132+ and return all of its contents as a string.
133+ """
134+ return Path (
135+ f"{ self .cdr .regression_directory } /{ modname } /{ cmd } " ).read_text ()
136+
137+ def generate_output_for_commands (self , cmds : List [str ],
138+ dirpath : str ) -> None :
139+ """
140+ Takes a list of SDB commands in string form, invokes them in the
141+ context of the current crash dump/sdb.REPL, and stores their output
142+ in the directory specified, each under a different file.
143+
144+ Note: Keep in mind that if the directory specified exists then
145+ it will be removed together with all of its contents.
146+ """
147+ assert self .setup_target ()
148+ if os .path .exists (dirpath ):
149+ shutil .rmtree (dirpath )
150+ os .makedirs (dirpath )
151+ for cmd in cmds :
152+ with open (f"{ dirpath } /{ cmd } " , 'w' ) as f :
153+ with redirect_stdout (f ):
154+ self .repl_invoke (cmd )
155+
156+ def generate_output_for_test_module (self , modname : str ) -> None :
157+ """
158+ Generates the regression output for all the commands of
159+ a test module given module name. The assumption for this
160+ to work automatically is that for the given modname "mod"
161+ there exist a test module under test.integration named
162+ test_mod_generic which has a list of commands in string
163+ form called CMD_TABLE.
164+ """
165+ test_mod = import_module (f"tests.integration.test_{ modname } _generic" )
166+ self .generate_output_for_commands (
167+ test_mod .CMD_TABLE , # type: ignore[attr-defined]
168+ f"{ self .cdr .regression_directory } /{ modname } " )
169+ print (f"Generated regression test output for { modname } ..." )
170+
171+ def generate_known_regression_output (self ) -> None :
172+ """
173+ Auto-generate the baseline regression output for all
174+ the detected test modules in this directory.
175+ """
176+ for modname in get_all_generic_test_modules ():
177+ self .generate_output_for_test_module (modname )
0 commit comments