xref: /freebsd/libexec/atf/atf-pytest-wrapper/atf_pytest_wrapper.cpp (revision 015d570cc929b7cdc45756aaeb6c870ac5f8ab22)
1 #include <format>
2 #include <iostream>
3 #include <map>
4 #include <string>
5 #include <vector>
6 #include <stdlib.h>
7 #include <unistd.h>
8 
9 class Handler {
10   private:
11     const std::string kPytestName = "pytest";
12     const std::string kCleanupSuffix = ":cleanup";
13     const std::string kPythonPathEnv = "PYTHONPATH";
14     const std::string kAtfVar = "_ATF_VAR_";
15   public:
16     // Test listing requested
17     bool flag_list = false;
18     // Output debug data (will break listing)
19     bool flag_debug = false;
20     // Cleanup for the test requested
21     bool flag_cleanup = false;
22     // Test source directory (provided by ATF)
23     std::string src_dir;
24     // Path to write test status to (provided by ATF)
25     std::string dst_file;
26     // Path to add to PYTHONPATH (provided by the schebang args)
27     std::string python_path;
28     // Path to the script (provided by the schebang wrapper)
29     std::string script_path;
30     // Name of the test to run (provided by ATF)
31     std::string test_name;
32     // kv pairs (provided by ATF)
33     std::map<std::string,std::string> kv_map;
34     // our binary name
35     std::string binary_name;
36 
37     static std::vector<std::string> ToVector(int argc, char **argv) {
38       std::vector<std::string> ret;
39 
40       for (int i = 0; i < argc; i++) {
41         ret.emplace_back(std::string(argv[i]));
42       }
43       return ret;
44     }
45 
46     static void PrintVector(std::string prefix, const std::vector<std::string> &vec) {
47       std::cerr << prefix << ": ";
48       for (auto &val: vec) {
49         std::cerr << "'" << val << "' ";
50       }
51       std::cerr << std::endl;
52     }
53 
54     void Usage(std::string msg, bool exit_with_error) {
55       std::cerr << binary_name << ": ERROR: " << msg << "." << std::endl;
56       std::cerr << binary_name << ": See atf-test-program(1) for usage details." << std::endl;
57       exit(exit_with_error != 0);
58     }
59 
60     // Parse args received from the OS. There can be multiple valid options:
61     // * with schebang args (#!/binary -P/path):
62     // atf_wrap '-P /path' /path/to/script -l
63     // * without schebang args
64     // atf_wrap /path/to/script -l
65     // Running test:
66     // atf_wrap '-P /path' /path/to/script -r /path1 -s /path2 -vk1=v1 testname
67     void Parse(int argc, char **argv) {
68       if (flag_debug) {
69         PrintVector("IN", ToVector(argc, argv));
70       }
71       // getopt() skips the first argument (as it is typically binary name)
72       // it is possible to have either '-P\s*/path' followed by the script name
73       // or just the script name. Parse kernel-provided arg manually and adjust
74       // array to make getopt work
75 
76       binary_name = std::string(argv[0]);
77       argc--; argv++;
78       // parse -P\s*path from the kernel.
79       if (argc > 0 && !strncmp(argv[0], "-P", 2)) {
80         char *path = &argv[0][2];
81         while (*path == ' ')
82           path++;
83         python_path = std::string(path);
84         argc--; argv++;
85       }
86 
87       // The next argument is a script name. Copy and keep argc/argv the same
88       // Show usage for empty args
89       if (argc == 0) {
90           Usage("Must provide a test case name", true);
91       }
92       script_path = std::string(argv[0]);
93 
94       int c;
95       while ((c = getopt(argc, argv, "lr:s:v:")) != -1) {
96         switch (c) {
97         case 'l':
98           flag_list = true;
99           break;
100         case 's':
101           src_dir = std::string(optarg);
102           break;
103         case 'r':
104           dst_file = std::string(optarg);
105           break;
106         case 'v':
107 	  {
108 	    std::string kv = std::string(optarg);
109 	    size_t splitter = kv.find("=");
110 	    if (splitter == std::string::npos) {
111 	      Usage("Unknown variable: " + kv, true);
112 	    }
113 	    kv_map[kv.substr(0, splitter)] = kv.substr(splitter + 1);
114 	  }
115           break;
116         default:
117           Usage("Unknown option -" + std::string(1, static_cast<char>(c)), true);
118         }
119       }
120       argc -= optind;
121       argv += optind;
122 
123       if (flag_list) {
124         return;
125       }
126       // There should be just one argument with the test name
127       if (argc != 1) {
128         Usage("Must provide a test case name", true);
129       }
130       test_name = std::string(argv[0]);
131       if (test_name.size() > kCleanupSuffix.size() &&
132           std::equal(kCleanupSuffix.rbegin(), kCleanupSuffix.rend(), test_name.rbegin())) {
133         test_name = test_name.substr(0, test_name.size() - kCleanupSuffix.size());
134         flag_cleanup = true;
135       }
136     }
137 
138     std::vector<std::string> BuildArgs() {
139       std::vector<std::string> args = {"pytest", "-p", "no:cacheprovider", "-s", "--atf"};
140 
141       if (flag_list) {
142         args.push_back("--co");
143         args.push_back(script_path);
144         return args;
145       }
146       if (flag_cleanup) {
147         args.push_back("--atf-cleanup");
148       }
149       // workaround pytest parser bug:
150       // https://github.com/pytest-dev/pytest/issues/3097
151       // use '--arg=value' format instead of '--arg value' for all
152       // path-like options
153       if (!src_dir.empty()) {
154         args.push_back("--atf-source-dir=" + src_dir);
155       }
156       if (!dst_file.empty()) {
157         args.push_back("--atf-file=" + dst_file);
158       }
159       // Create nodeid from the test path &name
160       args.push_back(script_path + "::" + test_name);
161       return args;
162     }
163 
164     void SetPythonPath() {
165       if (!python_path.empty()) {
166         char *env_path = getenv(kPythonPathEnv.c_str());
167         if (env_path != nullptr) {
168           python_path = python_path + ":" + std::string(env_path);
169         }
170         setenv(kPythonPathEnv.c_str(), python_path.c_str(), 1);
171       }
172     }
173 
174     void SetEnv() {
175       SetPythonPath();
176 
177       // Pass ATF kv pairs as env variables to avoid dealing with
178       // pytest parser
179       for (auto [k, v]: kv_map) {
180 	setenv((kAtfVar + k).c_str(), v.c_str(), 1);
181       }
182     }
183 
184     int Run(std::string binary, std::vector<std::string> args) {
185       if (flag_debug) {
186         PrintVector("OUT", args);
187       }
188       // allocate array with final NULL
189       char **arr = new char*[args.size() + 1]();
190       for (unsigned long i = 0; i < args.size(); i++) {
191 	// work around 'char *const *'
192         arr[i] = strdup(args[i].c_str());
193       }
194       return (execvp(binary.c_str(), arr) != 0);
195     }
196 
197     int Process() {
198       SetEnv();
199       return Run(kPytestName, BuildArgs());
200     }
201 };
202 
203 
204 int main(int argc, char **argv) {
205   Handler handler;
206 
207   handler.Parse(argc, argv);
208   return handler.Process();
209 }
210