1 /*
2  * Copyright (c) 2016-present, Yann Collet, Facebook, Inc.
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. */
is_name_bad(char const * name)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. */
are_names_bad()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. */
required_message(option_type opt_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. */
print_help(void)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. */
parse_args(int argc,char ** argv)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 
tprint_names(FILE * results,char const * data_name,char const * config_name,char const * method_name)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  */
run_all(FILE * results)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. */
diff_results(char const * actual_file,char const * expected_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 
main(int argc,char ** argv)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 }
363