648db22b |
1 | /* |
2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
3 | * All rights reserved. |
4 | * |
5 | * This source code is licensed under both the BSD-style license (found in the |
6 | * LICENSE file in the root directory of this source tree) and the GPLv2 (found |
7 | * in the COPYING file in the root directory of this source tree). |
8 | * You may select, at your option, one of the above-listed licenses. |
9 | */ |
10 | |
11 | #include <assert.h> |
12 | #include <getopt.h> |
13 | #include <stdio.h> |
14 | #include <string.h> |
15 | |
16 | #include "config.h" |
17 | #include "data.h" |
18 | #include "method.h" |
19 | |
20 | static int g_max_name_len = 0; |
21 | |
22 | /** Check if a name contains a comma or is too long. */ |
23 | static int is_name_bad(char const* name) { |
24 | if (name == NULL) |
25 | return 1; |
26 | int const len = strlen(name); |
27 | if (len > g_max_name_len) |
28 | g_max_name_len = len; |
29 | for (; *name != '\0'; ++name) |
30 | if (*name == ',') |
31 | return 1; |
32 | return 0; |
33 | } |
34 | |
35 | /** Check if any of the names contain a comma. */ |
36 | static int are_names_bad() { |
37 | for (size_t method = 0; methods[method] != NULL; ++method) |
38 | if (is_name_bad(methods[method]->name)) { |
39 | fprintf(stderr, "method name %s is bad\n", methods[method]->name); |
40 | return 1; |
41 | } |
42 | for (size_t datum = 0; data[datum] != NULL; ++datum) |
43 | if (is_name_bad(data[datum]->name)) { |
44 | fprintf(stderr, "data name %s is bad\n", data[datum]->name); |
45 | return 1; |
46 | } |
47 | for (size_t config = 0; configs[config] != NULL; ++config) |
48 | if (is_name_bad(configs[config]->name)) { |
49 | fprintf(stderr, "config name %s is bad\n", configs[config]->name); |
50 | return 1; |
51 | } |
52 | return 0; |
53 | } |
54 | |
55 | /** |
56 | * Option parsing using getopt. |
57 | * When you add a new option update: long_options, long_extras, and |
58 | * short_options. |
59 | */ |
60 | |
61 | /** Option variables filled by parse_args. */ |
62 | static char const* g_output = NULL; |
63 | static char const* g_diff = NULL; |
64 | static char const* g_cache = NULL; |
65 | static char const* g_zstdcli = NULL; |
66 | static char const* g_config = NULL; |
67 | static char const* g_data = NULL; |
68 | static char const* g_method = NULL; |
69 | |
70 | typedef enum { |
71 | required_option, |
72 | optional_option, |
73 | help_option, |
74 | } option_type; |
75 | |
76 | /** |
77 | * Extra state that we need to keep per-option that we can't store in getopt. |
78 | */ |
79 | struct option_extra { |
80 | int id; /**< The short option name, used as an id. */ |
81 | char const* help; /**< The help message. */ |
82 | option_type opt_type; /**< The option type: required, optional, or help. */ |
83 | char const** value; /**< The value to set or NULL if no_argument. */ |
84 | }; |
85 | |
86 | /** The options. */ |
87 | static struct option long_options[] = { |
88 | {"cache", required_argument, NULL, 'c'}, |
89 | {"output", required_argument, NULL, 'o'}, |
90 | {"zstd", required_argument, NULL, 'z'}, |
91 | {"config", required_argument, NULL, 128}, |
92 | {"data", required_argument, NULL, 129}, |
93 | {"method", required_argument, NULL, 130}, |
94 | {"diff", required_argument, NULL, 'd'}, |
95 | {"help", no_argument, NULL, 'h'}, |
96 | }; |
97 | |
98 | static size_t const nargs = sizeof(long_options) / sizeof(long_options[0]); |
99 | |
100 | /** The extra info for the options. Must be in the same order as the options. */ |
101 | static struct option_extra long_extras[] = { |
102 | {'c', "the cache directory", required_option, &g_cache}, |
103 | {'o', "write the results here", required_option, &g_output}, |
104 | {'z', "zstd cli tool", required_option, &g_zstdcli}, |
105 | {128, "use this config", optional_option, &g_config}, |
106 | {129, "use this data", optional_option, &g_data}, |
107 | {130, "use this method", optional_option, &g_method}, |
108 | {'d', "compare the results to this file", optional_option, &g_diff}, |
109 | {'h', "display this message", help_option, NULL}, |
110 | }; |
111 | |
112 | /** The short options. Must correspond to the options. */ |
113 | static char const short_options[] = "c:d:ho:z:"; |
114 | |
115 | /** Return the help string for the option type. */ |
116 | static char const* required_message(option_type opt_type) { |
117 | switch (opt_type) { |
118 | case required_option: |
119 | return "[required]"; |
120 | case optional_option: |
121 | return "[optional]"; |
122 | case help_option: |
123 | return ""; |
124 | default: |
125 | assert(0); |
126 | return NULL; |
127 | } |
128 | } |
129 | |
130 | /** Print the help for the program. */ |
131 | static void print_help(void) { |
132 | fprintf(stderr, "regression test runner\n"); |
133 | size_t const nargs = sizeof(long_options) / sizeof(long_options[0]); |
134 | for (size_t i = 0; i < nargs; ++i) { |
135 | if (long_options[i].val < 128) { |
136 | /* Long / short - help [option type] */ |
137 | fprintf( |
138 | stderr, |
139 | "--%s / -%c \t- %s %s\n", |
140 | long_options[i].name, |
141 | long_options[i].val, |
142 | long_extras[i].help, |
143 | required_message(long_extras[i].opt_type)); |
144 | } else { |
145 | /* Short / long - help [option type] */ |
146 | fprintf( |
147 | stderr, |
148 | "--%s \t- %s %s\n", |
149 | long_options[i].name, |
150 | long_extras[i].help, |
151 | required_message(long_extras[i].opt_type)); |
152 | } |
153 | } |
154 | } |
155 | |
156 | /** Parse the arguments. Return 0 on success. Print help on failure. */ |
157 | static int parse_args(int argc, char** argv) { |
158 | int option_index = 0; |
159 | int c; |
160 | |
161 | while (1) { |
162 | c = getopt_long(argc, argv, short_options, long_options, &option_index); |
163 | if (c == -1) |
164 | break; |
165 | |
166 | int found = 0; |
167 | for (size_t i = 0; i < nargs; ++i) { |
168 | if (c == long_extras[i].id && long_extras[i].value != NULL) { |
169 | *long_extras[i].value = optarg; |
170 | found = 1; |
171 | break; |
172 | } |
173 | } |
174 | if (found) |
175 | continue; |
176 | |
177 | switch (c) { |
178 | case 'h': |
179 | case '?': |
180 | default: |
181 | print_help(); |
182 | return 1; |
183 | } |
184 | } |
185 | |
186 | int bad = 0; |
187 | for (size_t i = 0; i < nargs; ++i) { |
188 | if (long_extras[i].opt_type != required_option) |
189 | continue; |
190 | if (long_extras[i].value == NULL) |
191 | continue; |
192 | if (*long_extras[i].value != NULL) |
193 | continue; |
194 | fprintf( |
195 | stderr, |
196 | "--%s is a required argument but is not set\n", |
197 | long_options[i].name); |
198 | bad = 1; |
199 | } |
200 | if (bad) { |
201 | fprintf(stderr, "\n"); |
202 | print_help(); |
203 | return 1; |
204 | } |
205 | |
206 | return 0; |
207 | } |
208 | |
209 | /** Helper macro to print to stderr and a file. */ |
210 | #define tprintf(file, ...) \ |
211 | do { \ |
212 | fprintf(file, __VA_ARGS__); \ |
213 | fprintf(stderr, __VA_ARGS__); \ |
214 | } while (0) |
215 | /** Helper macro to flush stderr and a file. */ |
216 | #define tflush(file) \ |
217 | do { \ |
218 | fflush(file); \ |
219 | fflush(stderr); \ |
220 | } while (0) |
221 | |
222 | void tprint_names( |
223 | FILE* results, |
224 | char const* data_name, |
225 | char const* config_name, |
226 | char const* method_name) { |
227 | int const data_padding = g_max_name_len - strlen(data_name); |
228 | int const config_padding = g_max_name_len - strlen(config_name); |
229 | int const method_padding = g_max_name_len - strlen(method_name); |
230 | |
231 | tprintf( |
232 | results, |
233 | "%s, %*s%s, %*s%s, %*s", |
234 | data_name, |
235 | data_padding, |
236 | "", |
237 | config_name, |
238 | config_padding, |
239 | "", |
240 | method_name, |
241 | method_padding, |
242 | ""); |
243 | } |
244 | |
245 | /** |
246 | * Run all the regression tests and record the results table to results and |
247 | * stderr progressively. |
248 | */ |
249 | static int run_all(FILE* results) { |
250 | tprint_names(results, "Data", "Config", "Method"); |
251 | tprintf(results, "Total compressed size\n"); |
252 | for (size_t method = 0; methods[method] != NULL; ++method) { |
253 | if (g_method != NULL && strcmp(methods[method]->name, g_method)) |
254 | continue; |
255 | for (size_t datum = 0; data[datum] != NULL; ++datum) { |
256 | if (g_data != NULL && strcmp(data[datum]->name, g_data)) |
257 | continue; |
258 | /* Create the state common to all configs */ |
259 | method_state_t* state = methods[method]->create(data[datum]); |
260 | for (size_t config = 0; configs[config] != NULL; ++config) { |
261 | if (g_config != NULL && strcmp(configs[config]->name, g_config)) |
262 | continue; |
263 | if (config_skip_data(configs[config], data[datum])) |
264 | continue; |
265 | /* Print the result for the (method, data, config) tuple. */ |
266 | result_t const result = |
267 | methods[method]->compress(state, configs[config]); |
268 | if (result_is_skip(result)) |
269 | continue; |
270 | tprint_names( |
271 | results, |
272 | data[datum]->name, |
273 | configs[config]->name, |
274 | methods[method]->name); |
275 | if (result_is_error(result)) { |
276 | tprintf(results, "%s\n", result_get_error_string(result)); |
277 | } else { |
278 | tprintf( |
279 | results, |
280 | "%llu\n", |
281 | (unsigned long long)result_get_data(result).total_size); |
282 | } |
283 | tflush(results); |
284 | } |
285 | methods[method]->destroy(state); |
286 | } |
287 | } |
288 | return 0; |
289 | } |
290 | |
291 | /** memcmp() the old results file and the new results file. */ |
292 | static int diff_results(char const* actual_file, char const* expected_file) { |
293 | data_buffer_t const actual = data_buffer_read(actual_file); |
294 | data_buffer_t const expected = data_buffer_read(expected_file); |
295 | int ret = 1; |
296 | |
297 | if (actual.data == NULL) { |
298 | fprintf(stderr, "failed to open results '%s' for diff\n", actual_file); |
299 | goto out; |
300 | } |
301 | if (expected.data == NULL) { |
302 | fprintf( |
303 | stderr, |
304 | "failed to open previous results '%s' for diff\n", |
305 | expected_file); |
306 | goto out; |
307 | } |
308 | |
309 | ret = data_buffer_compare(actual, expected); |
310 | if (ret != 0) { |
311 | fprintf( |
312 | stderr, |
313 | "actual results '%s' does not match expected results '%s'\n", |
314 | actual_file, |
315 | expected_file); |
316 | } else { |
317 | fprintf(stderr, "actual results match expected results\n"); |
318 | } |
319 | out: |
320 | data_buffer_free(actual); |
321 | data_buffer_free(expected); |
322 | return ret; |
323 | } |
324 | |
325 | int main(int argc, char** argv) { |
326 | /* Parse args and validate modules. */ |
327 | int ret = parse_args(argc, argv); |
328 | if (ret != 0) |
329 | return ret; |
330 | |
331 | if (are_names_bad()) |
332 | return 1; |
333 | |
334 | /* Initialize modules. */ |
335 | method_set_zstdcli(g_zstdcli); |
336 | ret = data_init(g_cache); |
337 | if (ret != 0) { |
338 | fprintf(stderr, "data_init() failed with error=%s\n", strerror(ret)); |
339 | return 1; |
340 | } |
341 | |
342 | /* Run the regression tests. */ |
343 | ret = 1; |
344 | FILE* results = fopen(g_output, "w"); |
345 | if (results == NULL) { |
346 | fprintf(stderr, "Failed to open the output file\n"); |
347 | goto out; |
348 | } |
349 | ret = run_all(results); |
350 | fclose(results); |
351 | |
352 | if (ret != 0) |
353 | goto out; |
354 | |
355 | if (g_diff) |
356 | /* Diff the new results with the previous results. */ |
357 | ret = diff_results(g_output, g_diff); |
358 | |
359 | out: |
360 | data_finish(); |
361 | return ret; |
362 | } |