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 } 363