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