├── .gitignore ├── example.png ├── example2.png ├── draw_internal.h ├── args.h ├── input.h ├── ascii.h ├── regression.h ├── test_guff.h ├── fnv.h ├── input_internal.h ├── counter.h ├── test_guff.c ├── svg.h ├── fnv.c ├── scale.h ├── draw.h ├── LICENSE ├── types.h ├── test_types.c ├── guff.h ├── scale.c ├── example.svg ├── Makefile ├── example2.svg ├── discretion ├── main.c ├── counter.c ├── regression.c ├── man ├── guff.1.ronn ├── guff.1 └── guff.1.html ├── test_regression.c ├── ascii.c ├── draw.c ├── README.md ├── test_scale.c ├── test_draw.c ├── input.c ├── args.c ├── svg.c ├── test_input.c └── greatest.h /.gitignore: -------------------------------------------------------------------------------- 1 | guff 2 | test_guff 3 | *.o 4 | 5 | afl/ 6 | testdata/ 7 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentbicycle/guff/HEAD/example.png -------------------------------------------------------------------------------- /example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentbicycle/guff/HEAD/example2.png -------------------------------------------------------------------------------- /draw_internal.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAW_INTERNAL_H 2 | #define DRAW_INTERNAL_H 3 | 4 | #endif 5 | -------------------------------------------------------------------------------- /args.h: -------------------------------------------------------------------------------- 1 | #ifndef ARGS_H 2 | #define ARGS_H 3 | 4 | #include "guff.h" 5 | 6 | void args_handle(config *cfg, int argc, char **argv); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /input.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_H 2 | #define INPUT_H 3 | 4 | #include "guff.h" 5 | 6 | int input_read(config *cfg, data_set *ds); 7 | void input_free(data_set *ds); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /ascii.h: -------------------------------------------------------------------------------- 1 | #ifndef ASCII_H 2 | #define ASCII_H 3 | 4 | #include "guff.h" 5 | #include "draw.h" 6 | 7 | int ascii_plot(config *cfg, plot_info *pi, data_set *ds); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /regression.h: -------------------------------------------------------------------------------- 1 | #ifndef REGRESSION_H 2 | #define REGRESSION_H 3 | 4 | #include "guff.h" 5 | #include "scale.h" 6 | 7 | /* Linear regression. */ 8 | void regression(point *points, size_t point_count, transform_t t, double *slope, double *intercept); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /test_guff.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_GUFF_H 2 | #define TEST_GUFF_H 3 | 4 | #include "greatest.h" 5 | #include "guff.h" 6 | 7 | #define DEF_TEST(X) TEST X(void) 8 | 9 | SUITE(s_draw); 10 | SUITE(s_input); 11 | SUITE(s_regression); 12 | SUITE(s_scale); 13 | 14 | extern greatest_type_info *type_point; 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /fnv.h: -------------------------------------------------------------------------------- 1 | #ifndef FNV_H 2 | #define FNV_H 3 | 4 | #include 5 | #include 6 | 7 | /* Fowler/Noll/Vo hash, 64-bit FNV-1a. 8 | * This hashing algorithm is in the public domain. 9 | * For more details, see: http://www.isthe.com/chongo/tech/comp/fnv/. */ 10 | uint64_t fnv1a(uint8_t *buf, size_t buf_size); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /input_internal.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_INTERNAL_H 2 | #define INPUT_INTERNAL_H 3 | 4 | typedef enum { 5 | SINK_LINE_OK, 6 | SINK_LINE_EMPTY, 7 | SINK_LINE_COMMENT, 8 | SINK_LINE_DONE, 9 | } sink_line_res; 10 | 11 | void init_pairs(data_set *ds); 12 | sink_line_res sink_line(config *cfg, data_set *ds, char *line, size_t len, size_t row_count); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /counter.h: -------------------------------------------------------------------------------- 1 | #ifndef COUNTER_H 2 | #define COUNTER_H 3 | 4 | #include "guff.h" 5 | 6 | typedef struct counter counter; 7 | 8 | /* Init a counter table with sufficient space for ROWS cells. */ 9 | counter *counter_init(size_t rows); 10 | 11 | void counter_increment(counter *c, size_t x, size_t y); 12 | 13 | size_t counter_get(counter *c, size_t x, size_t y); 14 | 15 | void counter_free(counter *c); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /test_guff.c: -------------------------------------------------------------------------------- 1 | #include "test_guff.h" 2 | 3 | /* Add all the definitions that need to be in the test runner's main file. */ 4 | GREATEST_MAIN_DEFS(); 5 | 6 | int main(int argc, char **argv) { 7 | GREATEST_MAIN_BEGIN(); /* command-line arguments, initialization. */ 8 | RUN_SUITE(s_input); 9 | RUN_SUITE(s_draw); 10 | RUN_SUITE(s_regression); 11 | RUN_SUITE(s_scale); 12 | GREATEST_MAIN_END(); /* display results */ 13 | } 14 | -------------------------------------------------------------------------------- /svg.h: -------------------------------------------------------------------------------- 1 | #ifndef SVG_H 2 | #define SVG_H 3 | 4 | #include "guff.h" 5 | #include "draw.h" 6 | 7 | #define SVG_COLOR_COUNT 9 8 | #define SVG_DEF_POINT_SIZE 2 9 | 10 | typedef struct svg_theme { 11 | char *bg_color; 12 | char *border_color; 13 | char *axis_color; 14 | char *colors[SVG_COLOR_COUNT]; 15 | uint8_t line_width; 16 | uint8_t axis_width; 17 | uint8_t border_width; 18 | } svg_theme; 19 | 20 | int svg_plot(config *cfg, plot_info *pi, data_set *ds); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /fnv.c: -------------------------------------------------------------------------------- 1 | #include "fnv.h" 2 | 3 | /* Fowler/Noll/Vo hash, 64-bit FNV-1a. 4 | * This hashing algorithm is in the public domain. 5 | * For more details, see: http://www.isthe.com/chongo/tech/comp/fnv/. */ 6 | 7 | static const uint64_t fnv64_prime = 1099511628211L; 8 | static const uint64_t fnv64_offset_basis = 14695981039346656037UL; 9 | 10 | uint64_t fnv1a(uint8_t *buf, size_t buf_size) { 11 | uint64_t h = fnv64_offset_basis; 12 | for (size_t i = 0; i < buf_size; i++) { 13 | h = (h ^ buf[i]) * fnv64_prime; 14 | } 15 | return h; 16 | } 17 | -------------------------------------------------------------------------------- /scale.h: -------------------------------------------------------------------------------- 1 | #ifndef SCALE_H 2 | #define SCALE_H 3 | 4 | #include "guff.h" 5 | #include "draw.h" 6 | 7 | typedef struct { 8 | int32_t x; 9 | int32_t y; 10 | } scaled_point; 11 | 12 | typedef enum { 13 | TRANSFORM_NONE = 0, 14 | TRANSFORM_LOG_X = 1, 15 | TRANSFORM_LOG_Y = 2, 16 | TRANSFORM_LOG_XY = 3, 17 | } transform_t; 18 | 19 | void scale_point(plot_info *pi, point *p, scaled_point *out_p, transform_t t); 20 | 21 | transform_t scale_get_transform(bool log_x, bool log_y); 22 | void scale_transform(point *p, transform_t t, point *out); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /draw.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAW_H 2 | #define DRAW_H 3 | 4 | #include "guff.h" 5 | 6 | typedef struct { 7 | double min_x; 8 | double max_x; 9 | double min_y; 10 | double max_y; 11 | 12 | double range_x; 13 | double range_y; 14 | 15 | bool log_x; 16 | bool log_y; 17 | size_t w; 18 | size_t h; 19 | 20 | struct counter **counters; 21 | 22 | char **rows; 23 | 24 | bool draw_x_axis; 25 | bool draw_y_axis; 26 | size_t axis_x; 27 | size_t axis_y; 28 | } plot_info; 29 | 30 | int draw(config *cfg, data_set *ds); 31 | void draw_scale_point(plot_info *pi, point *p, size_t *out_x, size_t *out_y); 32 | void draw_calc_bounds(data_set *ds, plot_info *pi); 33 | void draw_calc_axis_pos(plot_info *pi); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Scott Vokes 2 | All rights reserved. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | typedef struct { 5 | double x; 6 | double y; 7 | } point; 8 | 9 | typedef struct { 10 | uint8_t row_ceil2; 11 | uint8_t columns; 12 | size_t rows; 13 | point **pairs; // p[col] -> p[row] 14 | } data_set; 15 | 16 | typedef enum { 17 | PLOT_ASCII, 18 | PLOT_SVG, 19 | } output_t; 20 | 21 | typedef enum { 22 | MODE_DOT, 23 | MODE_COUNT, 24 | MODE_LINE, 25 | } plot_t; 26 | 27 | typedef struct { 28 | bool log_x; 29 | bool log_y; 30 | bool log_count; 31 | bool flip_xy; 32 | bool x_column; 33 | plot_t mode; 34 | bool axis; 35 | bool stream_mode; 36 | bool colorblind; 37 | bool regression; 38 | size_t width; 39 | size_t height; 40 | char *in_path; 41 | FILE *in; 42 | output_t plot_type; 43 | 44 | struct svg_theme *svg_theme; 45 | } config; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /test_types.c: -------------------------------------------------------------------------------- 1 | #include "test_guff.h" 2 | 3 | static int point_printf_cb(const void *v, void *udata) { 4 | point *p = (point *)v; 5 | char buf[256]; 6 | int res = snprintf(buf, 256, "(%g, %g)", p->x, p->y); 7 | if (256 < res) { return -1; } 8 | printf("%s", buf); 9 | return res; 10 | } 11 | 12 | static int point_equal_cb(const void *vexp, const void *vgot, void *udata) { 13 | point *exp = (point *)vexp; 14 | point *got = (point *)vgot; 15 | double tol = 0.000000001; 16 | if (udata != NULL) { tol = *(double *)udata; } 17 | 18 | if (IS_EMPTY(exp->x) && !IS_EMPTY(got->x)) { 19 | return 0; 20 | } else if (fabs(exp->x - got->x) > tol) { 21 | return 0; 22 | } 23 | 24 | if (IS_EMPTY(exp->y) && !IS_EMPTY(got->y)) { 25 | return 0; 26 | } else if (fabs(exp->y - got->y) > tol) { 27 | return 0; 28 | } 29 | 30 | return 1; 31 | } 32 | 33 | static greatest_type_info type_point_def = { 34 | .equal = point_equal_cb, 35 | .print = point_printf_cb, 36 | }; 37 | 38 | greatest_type_info *type_point = &type_point_def; 39 | -------------------------------------------------------------------------------- /guff.h: -------------------------------------------------------------------------------- 1 | #ifndef GUFF_H 2 | #define GUFF_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "types.h" 19 | 20 | /* Version 0.1.0. */ 21 | #define GUFF_VERSION_MAJOR 0 22 | #define GUFF_VERSION_MINOR 1 23 | #define GUFF_VERSION_PATCH 0 24 | #define GUFF_AUTHOR "Scott Vokes " 25 | 26 | /* Include the axes if they are within CROSS_PAD * the range from the 27 | * min or max values, otherwise omit them..*/ 28 | #define CROSS_PAD 2.0 29 | 30 | #define EMPTY_VALUE NAN 31 | #define IS_EMPTY(V) isnan(V) 32 | #define IS_EMPTY_POINT(P) (IS_EMPTY(P->x) || IS_EMPTY(P->y)) 33 | 34 | #ifdef DEBUG 35 | #define LOG(LVL, ...) \ 36 | do { \ 37 | if (DEBUG >= LVL) { \ 38 | fprintf(stderr, __VA_ARGS__); \ 39 | } \ 40 | } while(0) 41 | #else 42 | #define LOG(_, ...) 43 | #endif 44 | 45 | #define MAX_COLUMNS 255 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /scale.c: -------------------------------------------------------------------------------- 1 | #include "scale.h" 2 | 3 | /* Point scaling / transformations. */ 4 | 5 | void scale_point(plot_info *pi, point *p, scaled_point *out_p, transform_t t) { 6 | const uint8_t pad = 2; 7 | point tp; 8 | scale_transform(p, t, &tp); 9 | double x = tp.x; 10 | double y = tp.y; 11 | 12 | double cmp_pad = 0.001; 13 | assert(x >= pi->min_x - cmp_pad); 14 | assert(x <= pi->max_x + cmp_pad); 15 | 16 | double cell_w = pi->range_x / pi->w; 17 | double cell_h = pi->range_y / pi->h; 18 | 19 | out_p->x = (pi->w - pad) * ((x - pi->min_x + cell_w/2) / pi->range_x); 20 | 21 | int32_t oy = (pi->h - pad) * ((y - pi->min_y + cell_h/2) / pi->range_y); 22 | // flip y; 0 at bottom of plot 23 | out_p->y = pi->h - oy - 1; 24 | 25 | LOG(2, "range_x: %g, range_y: %g, cell_w: %g, cell_h: %g\n", 26 | pi->range_x, pi->range_y, cell_w, cell_h); 27 | LOG(2, "[%u, %d | %d] / [%zu, %zu]\n", 28 | out_p->x, oy, out_p->y, pi->w, pi->h); 29 | } 30 | 31 | transform_t scale_get_transform(bool log_x, bool log_y) { 32 | transform_t res = TRANSFORM_NONE; 33 | if (log_x) { res |= TRANSFORM_LOG_X; } 34 | if (log_y) { res |= TRANSFORM_LOG_Y; } 35 | return res; 36 | } 37 | 38 | void scale_transform(point *p, transform_t t, point *out) { 39 | if (t & TRANSFORM_LOG_X) { 40 | out->x = p->x == 0 ? 0 : log(p->x); 41 | } else { 42 | out->x = p->x; 43 | } 44 | if (t & TRANSFORM_LOG_Y) { 45 | out->y = p->y == 0 ? 0 : log(p->y); 46 | } else { 47 | out->y = p->y; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = guff 2 | OPTIMIZE = -O3 3 | WARN = -Wall -pedantic 4 | CSTD += -std=c99 5 | LDFLAGS += -lm 6 | #CDEFS= -DDEBUG=0 7 | CFLAGS += ${CSTD} -g ${WARN} ${CDEFS} ${CINCS} ${OPTIMIZE} 8 | 9 | TEST_CFLAGS = ${CFLAGS} 10 | TEST_LDFLAGS = ${LDFLAGS} 11 | 12 | all: test_${PROJECT} 13 | all: ${PROJECT} 14 | 15 | OBJS= args.o \ 16 | ascii.o \ 17 | counter.o \ 18 | draw.o \ 19 | fnv.o \ 20 | input.o \ 21 | regression.o \ 22 | scale.o \ 23 | svg.o \ 24 | 25 | TEST_OBJS= ${OBJS} \ 26 | test_draw.o \ 27 | test_input.o \ 28 | test_regression.o \ 29 | test_scale.o \ 30 | test_types.o \ 31 | 32 | # Basic targets 33 | 34 | ${PROJECT}: main.o ${OBJS} 35 | ${CC} -o $@ main.o ${OBJS} ${LDFLAGS} 36 | 37 | test_${PROJECT}: test_${PROJECT}.o ${TEST_OBJS} 38 | ${CC} -o $@ test_${PROJECT}.o ${TEST_LDFLAGS} \ 39 | ${TEST_OBJS} ${TEST_CFLAGS} 40 | 41 | test: ./test_${PROJECT} 42 | ./test_${PROJECT} 43 | 44 | clean: 45 | rm -f ${PROJECT} test_${PROJECT} *.o *.a *.core 46 | 47 | tags: TAGS 48 | TAGS: 49 | etags *.[ch] 50 | 51 | docs: man/${PROJECT}.1 man/${PROJECT}.1.html 52 | 53 | man/${PROJECT}.1: man/${PROJECT}.1.ronn 54 | ronn --roff $< 55 | 56 | man/${PROJECT}.1.html: man/${PROJECT}.1.ronn 57 | ronn --html $< 58 | 59 | 60 | # Dependencies 61 | *.o: *.h Makefile 62 | 63 | # Installation 64 | PREFIX ?= /usr/local 65 | INSTALL ?= install 66 | RM ?= rm 67 | MAN_DEST ?= ${PREFIX}/share/man 68 | 69 | install: 70 | ${INSTALL} -d ${PREFIX}/bin ${MAN_DEST}/man1/ 71 | ${INSTALL} -c ${PROJECT} ${PREFIX}/bin 72 | ${INSTALL} -c man/${PROJECT}.1 ${MAN_DEST}/man1/ 73 | 74 | uninstall: 75 | ${RM} -f ${PREFIX}/bin/${PROJECT} 76 | ${RM} -f ${MAN_DEST}/man1/${PROJECT}.1 77 | -------------------------------------------------------------------------------- /example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /discretion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # Copyright (c) 2015 Scott Vokes 3 | # 4 | # Permission to use, copy, modify, and/or distribute this software for any 5 | # purpose with or without fee is hereby granted, provided that the above 6 | # copyright notice and this permission notice appear in all copies. 7 | # 8 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | # Read a stream of lines containing SVG documents, broken up by blank lines 17 | # (as output by `guff -s`), and save them as timestamped .svg files. 18 | 19 | BEGIN { 20 | if (ARGC > 1) { 21 | name = ARGV[1] 22 | ARGC-- 23 | } else { 24 | name = "out" 25 | } 26 | 27 | timestamp_cmd = "date \"+%Y-%m-%dT%H:%M:%S%z\"" 28 | sec_i = 0 29 | next_file() 30 | } 31 | 32 | function get_timestamp( t) { 33 | timestamp_cmd | getline t 34 | 35 | # close the pipe from date(1), so the next date is fresh 36 | close(timestamp_cmd) 37 | return t 38 | } 39 | 40 | function next_file() { 41 | last_out_date = out_date 42 | out_date = get_timestamp() 43 | if (out_date == last_out_date) { 44 | sec_i++ 45 | } else { 46 | sec_i = 0 47 | } 48 | out_file = out_name(out_date, sec_i) 49 | clobber(out_file) 50 | } 51 | 52 | function clobber(name) { 53 | printf("") > name 54 | } 55 | 56 | function out_name(timestamp, i) { 57 | return sprintf("%s_%s_%d.svg", name, timestamp, i) 58 | } 59 | 60 | /^$/ { 61 | symlink_cmd = sprintf("ln -s -f '%s' '%s_newest.svg'", out_file, name) 62 | system(symlink_cmd) 63 | next_file() 64 | next 65 | } 66 | 67 | /.*/ { 68 | print($0) >> out_file 69 | } 70 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Scott Vokes 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include "guff.h" 18 | #include "args.h" 19 | #include "input.h" 20 | #include "draw.h" 21 | 22 | static void read_env(config *cfg) { 23 | if (getenv("GUFF_FLIP")) { cfg->flip_xy = true; } 24 | 25 | char *value = getenv("GUFF_WIDTH"); 26 | if (value) { cfg->width = atoi(value); } 27 | value = getenv("GUFF_HEIGHT"); 28 | if (value) { cfg->height = atoi(value); } 29 | } 30 | 31 | int main(int argc, char **argv) { 32 | config cfg = { 33 | .axis = true, 34 | .in = stdin, 35 | .stream_mode = true, 36 | }; 37 | 38 | read_env(&cfg); 39 | 40 | args_handle(&cfg, argc, argv); 41 | 42 | bool end_of_stream = false; 43 | while (!end_of_stream) { 44 | data_set ds = { .pairs = NULL }; 45 | int res = input_read(&cfg, &ds); 46 | if (res == -1) { 47 | end_of_stream = true; 48 | } else if (res != 0) { 49 | input_free(&ds); 50 | return res; 51 | } 52 | if (ds.rows == 0) { // no input 53 | input_free(&ds); 54 | return 0; 55 | } 56 | 57 | res = draw(&cfg, &ds); 58 | input_free(&ds); 59 | if (res != 0) { return res; } 60 | 61 | if (!end_of_stream) { printf("\n"); } 62 | } 63 | 64 | if (cfg.svg_theme) { free(cfg.svg_theme); } 65 | 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /counter.c: -------------------------------------------------------------------------------- 1 | #include "counter.h" 2 | #include "fnv.h" 3 | 4 | /* Hash table, for tracking point counts. */ 5 | 6 | typedef struct { 7 | size_t x; 8 | size_t y; 9 | size_t count; 10 | } bucket; 11 | 12 | struct counter { 13 | uint8_t bucket_ceil2; 14 | size_t bucket_count; 15 | bucket buckets[]; 16 | }; 17 | 18 | #define EMPTY_BUCKET ((size_t)-1) 19 | 20 | /* Alloc a hash table with 2*ceil(rows) buckets. */ 21 | counter *counter_init(size_t rows) { 22 | size_t ceil = rows; 23 | uint8_t ceil2 = 2; 24 | while ((1 << ceil2) < ceil) { ceil2++; } 25 | ceil2++; 26 | size_t bucket_count = 1 << ceil2; 27 | 28 | size_t size = sizeof(counter) + bucket_count * sizeof(bucket); 29 | counter *c = malloc(size); 30 | if (c) { 31 | c->bucket_ceil2 = ceil2; 32 | c->bucket_count = bucket_count; 33 | for (size_t i = 0; i < bucket_count; i++) { 34 | c->buckets[i].x = EMPTY_BUCKET; 35 | c->buckets[i].y = EMPTY_BUCKET; 36 | } 37 | } 38 | return c; 39 | } 40 | 41 | size_t point_hash(size_t x, size_t y) { 42 | size_t buf_size = 2*sizeof(size_t); 43 | uint8_t buf[buf_size]; 44 | memcpy(&buf[0], &x, sizeof(x)); 45 | memcpy(&buf[sizeof(x)], &y, sizeof(y)); 46 | return (size_t)fnv1a(buf, buf_size); 47 | } 48 | 49 | static bucket *find_bucket(counter *c, size_t x, size_t y) { 50 | size_t hash = point_hash(x, y); 51 | size_t mask = c->bucket_count - 1; 52 | for (size_t i = 0; i < c->bucket_count; i++) { 53 | size_t bi = (hash + i) & mask; 54 | bucket *b = &c->buckets[bi]; 55 | if (b->x == x && b->y == y) { 56 | return b; 57 | } else if (b->x == EMPTY_BUCKET) { 58 | b->x = x; 59 | b->y = y; 60 | b->count = 0; 61 | return b; 62 | } 63 | } 64 | return NULL; 65 | } 66 | 67 | void counter_increment(counter *c, size_t x, size_t y) { 68 | bucket *b = find_bucket(c, x, y); 69 | assert(b); 70 | b->count++; 71 | } 72 | 73 | size_t counter_get(counter *c, size_t x, size_t y) { 74 | bucket *b = find_bucket(c, x, y); 75 | assert(b); 76 | return b->count; 77 | } 78 | 79 | void counter_free(counter *c) { 80 | free(c); 81 | } 82 | -------------------------------------------------------------------------------- /regression.c: -------------------------------------------------------------------------------- 1 | #include "regression.h" 2 | #include "scale.h" 3 | 4 | /* Linear regression. */ 5 | 6 | // lr:{m:{(+/x)%1.0*#x};mx:m@x;my:m@y;dx:x-mx;n:+/dx*y-my;d:+/dx^2;s:n%d;(s;my-s*mx)} 7 | 8 | static bool calc_means(point *points, size_t point_count, transform_t t, double *mx, double *my); 9 | static double calc_num(point *points, size_t point_count, transform_t t, double mx, double my); 10 | static double calc_den(point *points, size_t point_count, transform_t t, double mx); 11 | 12 | void regression(point *points, size_t point_count, transform_t t, 13 | double *slope, double *intercept) { 14 | assert(slope); 15 | assert(intercept); 16 | 17 | double mx = 0; 18 | double my = 0; 19 | if (!calc_means(points, point_count, t, &mx, &my)) { 20 | *slope = EMPTY_VALUE; 21 | *intercept = EMPTY_VALUE; 22 | return; 23 | } 24 | 25 | double num = calc_num(points, point_count, t, mx, my); 26 | double den = calc_den(points, point_count, t, mx); 27 | *slope = num / den; 28 | *intercept = my - *slope * mx; 29 | 30 | LOG(1, "-- slope: %g, intercept: %g\n", *slope, *intercept); 31 | } 32 | 33 | static bool calc_means(point *points, size_t point_count, transform_t t, double *mx, double *my) { 34 | double tx = 0; 35 | double ty = 0; 36 | size_t cx = 0; 37 | size_t cy = 0; 38 | for (size_t i = 0; i < point_count; i++) { 39 | point *p = &points[i]; 40 | if (IS_EMPTY_POINT(p)) { continue; } 41 | cx++; 42 | cy++; 43 | point tp; 44 | scale_transform(p, t, &tp); 45 | tx += tp.x; 46 | ty += tp.y; 47 | } 48 | 49 | if (cx == 0) { return false; } 50 | *mx = tx / cx; 51 | *my = ty / cy; 52 | return true; 53 | } 54 | 55 | static double calc_num(point *points, size_t point_count, transform_t t, double mx, double my) { 56 | double sum = 0; 57 | for (size_t i = 0; i < point_count; i++) { 58 | point *p = &points[i]; 59 | point tp; 60 | scale_transform(p, t, &tp); 61 | sum += (tp.x - mx) * (tp.y - my); 62 | } 63 | return sum; 64 | } 65 | 66 | static double calc_den(point *points, size_t point_count, transform_t t, double mx) { 67 | double sum = 0; 68 | for (size_t i = 0; i < point_count; i++) { 69 | point *p = &points[i]; 70 | point tp; 71 | scale_transform(p, t, &tp); 72 | sum += (tp.x - mx) * (tp.x - mx); 73 | } 74 | return sum; 75 | } 76 | -------------------------------------------------------------------------------- /man/guff.1.ronn: -------------------------------------------------------------------------------- 1 | guff(1) -- a plot device 2 | ================================================= 3 | 4 | ## SYNOPSIS 5 | 6 | `guff` [-A] [-c] [-d WxH] [-f] [-h] [-l xyc] 7 | [-m MODE] [-r] [-s] [-S] [-x] [FILE] 8 | 9 | 10 | ## DESCRIPTION 11 | 12 | guff reads a stream of points from a file / stdin and plots them. 13 | 14 | 15 | ## OPTIONS 16 | 17 | Common options: 18 | 19 | * `-d WxH`: 20 | Set the dimensions (width and height). Should be formatted 21 | like "-d WxH", e.g. "-d 72x40" or "-d 640x480". 22 | 23 | * `-f`: 24 | Flip X and Y axes in plot. 25 | 26 | * `-h`: 27 | Print a help message. 28 | 29 | * `-l xyc`: 30 | Set X, Y, and/or Count to log-scale. 31 | 32 | * `-m MODE`: 33 | Set mode to dot (default), line (SVG only), or count (which 34 | tracks how densely clustered points are). 35 | 36 | * `-x`: 37 | Treat the first column as the X value for the other columns. 38 | Otherwise, the row number is used for the X value. 39 | 40 | SVG-only options: 41 | 42 | * `-c`: 43 | Use colorblind-safe default colors. 44 | 45 | * `-r`: 46 | Draw a linear regression line for each column. 47 | 48 | Rare options: 49 | 50 | * `-A`: 51 | Don't draw axes. 52 | 53 | * `-S`: 54 | Disable stream mode (exit at first blank line). 55 | 56 | 57 | ## EXIT STATUS 58 | 59 | Returns 0. 60 | 61 | 62 | ## EXAMPLES 63 | 64 | Read a series of number rows on stdin, and plot to ASCII once 65 | end-of-stream or a blank line is reached: 66 | 67 | $ guff 68 | 69 | Same, but generate SVG: 70 | 71 | $ guff -s 72 | 73 | Read number rows from a file: 74 | 75 | $ guff file 76 | 77 | Plot stdin with a log-scale on the Y axis: 78 | 79 | $ guff -ly 80 | 81 | Treat the first value on each line as the X value for all columns: 82 | 83 | $ guff -x 84 | 85 | Plot stdin to SVG, with lines connecting the points: 86 | 87 | $ guff -s -m line 88 | 89 | Same, but using a colorblind-safe palette: 90 | 91 | $ guff -s -c -m line 92 | 93 | Plot stdin to SVG, and draw a linear regression line for each column: 94 | 95 | $ guff -s -r 96 | 97 | Plot stdin with point counts, to show point density: 98 | 99 | $ guff -m count 100 | 101 | Same, with a log scale for the point counts: 102 | 103 | $ guff -m count -lc 104 | 105 | Same, with SVG output: 106 | 107 | $ guff -s -m count -lc 108 | 109 | 110 | ## BUGS 111 | 112 | There isn't a way to change the plot options when a blank line resets 113 | the data. 114 | 115 | Rather than attempt to intelligently handle strange input, guff just 116 | skips the rest of the line when strtod(3) indicates there isn't a 117 | well-formatted number. 118 | 119 | 120 | ## COPYRIGHT 121 | 122 | **guff** is Copyright (C) 2015 Scott Vokes . 123 | 124 | 125 | ## SEE ALSO 126 | 127 | awk(1) 128 | -------------------------------------------------------------------------------- /man/guff.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "GUFF" "1" "July 2015" "" "" 5 | . 6 | .SH "NAME" 7 | \fBguff\fR \- a plot device 8 | . 9 | .SH "SYNOPSIS" 10 | \fBguff\fR [\-A] [\-c] [\-d WxH] [\-f] [\-h] [\-l xyc] [\-m MODE] [\-r] [\-s] [\-S] [\-x] [FILE] 11 | . 12 | .SH "DESCRIPTION" 13 | guff reads a stream of points from a file / stdin and plots them\. 14 | . 15 | .SH "OPTIONS" 16 | Common options: 17 | . 18 | .TP 19 | \fB\-d WxH\fR 20 | Set the dimensions (width and height)\. Should be formatted like "\-d WxH", e\.g\. "\-d 72x40" or "\-d 640x480"\. 21 | . 22 | .TP 23 | \fB\-f\fR 24 | Flip X and Y axes in plot\. 25 | . 26 | .TP 27 | \fB\-h\fR 28 | Print a help message\. 29 | . 30 | .TP 31 | \fB\-l xyc\fR 32 | Set X, Y, and/or Count to log\-scale\. 33 | . 34 | .TP 35 | \fB\-m MODE\fR 36 | Set mode to dot (default), line (SVG only), or count (which tracks how densely clustered points are)\. 37 | . 38 | .TP 39 | \fB\-x\fR 40 | Treat the first column as the X value for the other columns\. Otherwise, the row number is used for the X value\. 41 | . 42 | .P 43 | SVG\-only options: 44 | . 45 | .TP 46 | \fB\-c\fR 47 | Use colorblind\-safe default colors\. 48 | . 49 | .TP 50 | \fB\-r\fR 51 | Draw a linear regression line for each column\. 52 | . 53 | .P 54 | Rare options: 55 | . 56 | .TP 57 | \fB\-A\fR 58 | Don\'t draw axes\. 59 | . 60 | .TP 61 | \fB\-S\fR 62 | Disable stream mode (exit at first blank line)\. 63 | . 64 | .SH "EXIT STATUS" 65 | Returns 0\. 66 | . 67 | .SH "EXAMPLES" 68 | Read a series of number rows on stdin, and plot to ASCII once end\-of\-stream or a blank line is reached: 69 | . 70 | .IP "" 4 71 | . 72 | .nf 73 | 74 | $ guff 75 | . 76 | .fi 77 | . 78 | .IP "" 0 79 | . 80 | .P 81 | Same, but generate SVG: 82 | . 83 | .IP "" 4 84 | . 85 | .nf 86 | 87 | $ guff \-s 88 | . 89 | .fi 90 | . 91 | .IP "" 0 92 | . 93 | .P 94 | Read number rows from a file: 95 | . 96 | .IP "" 4 97 | . 98 | .nf 99 | 100 | $ guff file 101 | . 102 | .fi 103 | . 104 | .IP "" 0 105 | . 106 | .P 107 | Plot stdin with a log\-scale on the Y axis: 108 | . 109 | .IP "" 4 110 | . 111 | .nf 112 | 113 | $ guff \-ly 114 | . 115 | .fi 116 | . 117 | .IP "" 0 118 | . 119 | .P 120 | Treat the first value on each line as the X value for all columns: 121 | . 122 | .IP "" 4 123 | . 124 | .nf 125 | 126 | $ guff \-x 127 | . 128 | .fi 129 | . 130 | .IP "" 0 131 | . 132 | .P 133 | Plot stdin to SVG, with lines connecting the points: 134 | . 135 | .IP "" 4 136 | . 137 | .nf 138 | 139 | $ guff \-s \-m line 140 | . 141 | .fi 142 | . 143 | .IP "" 0 144 | . 145 | .P 146 | Same, but using a colorblind\-safe palette: 147 | . 148 | .IP "" 4 149 | . 150 | .nf 151 | 152 | $ guff \-s \-c \-m line 153 | . 154 | .fi 155 | . 156 | .IP "" 0 157 | . 158 | .P 159 | Plot stdin to SVG, and draw a linear regression line for each column: 160 | . 161 | .IP "" 4 162 | . 163 | .nf 164 | 165 | $ guff \-s \-r 166 | . 167 | .fi 168 | . 169 | .IP "" 0 170 | . 171 | .P 172 | Plot stdin with point counts, to show point density: 173 | . 174 | .IP "" 4 175 | . 176 | .nf 177 | 178 | $ guff \-m count 179 | . 180 | .fi 181 | . 182 | .IP "" 0 183 | . 184 | .P 185 | Same, with a log scale for the point counts: 186 | . 187 | .IP "" 4 188 | . 189 | .nf 190 | 191 | $ guff \-m count \-lc 192 | . 193 | .fi 194 | . 195 | .IP "" 0 196 | . 197 | .P 198 | Same, with SVG output: 199 | . 200 | .IP "" 4 201 | . 202 | .nf 203 | 204 | $ guff \-s \-m count \-lc 205 | . 206 | .fi 207 | . 208 | .IP "" 0 209 | . 210 | .SH "BUGS" 211 | There isn\'t a way to change the plot options when a blank line resets the data\. 212 | . 213 | .P 214 | Rather than attempt to intelligently handle strange input, guff just skips the rest of the line when strtod(3) indicates there isn\'t a well\-formatted number\. 215 | . 216 | .SH "COPYRIGHT" 217 | \fBguff\fR is Copyright (C) 2015 Scott Vokes \fIvokes\.s@gmail\.com\fR\. 218 | . 219 | .SH "SEE ALSO" 220 | awk(1) 221 | -------------------------------------------------------------------------------- /test_regression.c: -------------------------------------------------------------------------------- 1 | #include "test_guff.h" 2 | 3 | #include "regression.h" 4 | #include 5 | 6 | static void setup_cb(void *data) { 7 | } 8 | 9 | static void teardown_cb(void *data) { 10 | } 11 | 12 | DEF_TEST(input_empty) { 13 | double slope = 0; 14 | double intercept = 0; 15 | regression(NULL, 0, TRANSFORM_NONE, &slope, &intercept); 16 | 17 | ASSERT(IS_EMPTY(slope)); 18 | ASSERT(IS_EMPTY(intercept)); 19 | 20 | PASS(); 21 | } 22 | 23 | DEF_TEST(input_simple) { 24 | double slope = 0; 25 | double intercept = 0; 26 | 27 | point points[] = { 28 | {.x = 0, .y = 10}, 29 | {.x = 1, .y = 15}, 30 | {.x = 2, .y = 20}, 31 | {.x = 3, .y = 25}, 32 | {.x = 4, .y = 30}, 33 | }; 34 | regression(points, 5, TRANSFORM_NONE, &slope, &intercept); 35 | 36 | ASSERT_IN_RANGE(5, slope, 0.001); 37 | ASSERT_IN_RANGE(10, intercept, 0.001); 38 | 39 | PASS(); 40 | } 41 | 42 | DEF_TEST(input_off_axis) { 43 | double slope = 0; 44 | double intercept = 0; 45 | 46 | point points[] = { 47 | {.x = 1000, .y = 1000}, 48 | {.x = 1010, .y = 1010}, 49 | {.x = 1020, .y = 1020}, 50 | {.x = 1030, .y = 1035}, 51 | {.x = 1040, .y = 1080}, 52 | }; 53 | regression(points, 5, TRANSFORM_NONE, &slope, &intercept); 54 | 55 | ASSERT_IN_RANGE(1.85, slope, 0.001); 56 | ASSERT_IN_RANGE(-858, intercept, 0.001); 57 | 58 | PASS(); 59 | } 60 | 61 | DEF_TEST(input_xy) { 62 | double slope = 0; 63 | double intercept = 0; 64 | 65 | point points[] = { 66 | {.x = -3, .y = -2}, 67 | {.x = -2, .y = -1}, 68 | {.x = 0, .y = 1}, 69 | {.x = 3, .y = 3}, 70 | {.x = 9, .y = 9}, 71 | }; 72 | regression(points, 5, TRANSFORM_NONE, &slope, &intercept); 73 | 74 | ASSERT_IN_RANGE(0.901, slope, 0.001); 75 | ASSERT_IN_RANGE(0.738197, intercept, 0.001); 76 | 77 | PASS(); 78 | } 79 | 80 | DEF_TEST(input_log_x) { 81 | double slope = 0; 82 | double intercept = 0; 83 | 84 | point points[] = { 85 | {.x = exp(0), .y = 0 + 50 }, 86 | {.x = exp(1), .y = 1 + 50 }, 87 | {.x = exp(2), .y = 2 + 50 }, 88 | {.x = exp(3), .y = 3 + 50 }, 89 | {.x = exp(4), .y = 4 + 50 }, 90 | }; 91 | regression(points, 5, TRANSFORM_LOG_X, &slope, &intercept); 92 | 93 | ASSERT(!isnan(slope)); 94 | ASSERT(!isnan(intercept)); 95 | ASSERT_IN_RANGE(1, slope, 0.001); 96 | ASSERT_IN_RANGE(50, intercept, 0.001); 97 | 98 | PASS(); 99 | } 100 | 101 | DEF_TEST(input_log_y_no_intercept) { 102 | double slope = 0; 103 | double intercept = 0; 104 | 105 | point points[] = { 106 | {.x = 0, .y = exp(0)}, 107 | {.x = 1, .y = exp(1)}, 108 | {.x = 2, .y = exp(2)}, 109 | {.x = 3, .y = exp(3)}, 110 | {.x = 4, .y = exp(4)}, 111 | }; 112 | regression(points, 5, TRANSFORM_LOG_Y, &slope, &intercept); 113 | 114 | ASSERT(!isnan(slope)); 115 | ASSERT(!isnan(intercept)); 116 | ASSERT_IN_RANGE(1, slope, 0.001); 117 | ASSERT_IN_RANGE(0, intercept, 0.001); 118 | 119 | PASS(); 120 | } 121 | 122 | DEF_TEST(input_log_y_intercept) { 123 | double slope = 0; 124 | double intercept = 0; 125 | 126 | point points[] = { 127 | {.x = 0, .y = exp(0) + 10 }, 128 | {.x = 1, .y = exp(1) + 10 }, 129 | {.x = 2, .y = exp(2) + 10 }, 130 | {.x = 3, .y = exp(3) + 10 }, 131 | {.x = 4, .y = exp(4) + 10 }, 132 | }; 133 | regression(points, 5, TRANSFORM_LOG_Y, &slope, &intercept); 134 | 135 | ASSERT(!isnan(slope)); 136 | ASSERT(!isnan(intercept)); 137 | ASSERT_IN_RANGE(0.440159, slope, 0.001); 138 | ASSERT_IN_RANGE(2.19348, intercept, 0.001); 139 | 140 | PASS(); 141 | } 142 | 143 | SUITE(s_regression) { 144 | SET_SETUP(setup_cb, NULL); 145 | SET_TEARDOWN(teardown_cb, NULL); 146 | 147 | RUN_TEST(input_empty); 148 | RUN_TEST(input_simple); 149 | RUN_TEST(input_off_axis); 150 | RUN_TEST(input_xy); 151 | RUN_TEST(input_log_x); 152 | RUN_TEST(input_log_y_no_intercept); 153 | RUN_TEST(input_log_y_intercept); 154 | } 155 | -------------------------------------------------------------------------------- /ascii.c: -------------------------------------------------------------------------------- 1 | #include "ascii.h" 2 | #include "counter.h" 3 | #include "scale.h" 4 | 5 | /* Plotting to ASCII. */ 6 | 7 | static void init_rows(plot_info *pi); 8 | static void draw_axes(plot_info *pi); 9 | static void print_header(plot_info *pi, bool point_counts, uint8_t columns); 10 | static char col_mark(uint8_t col); 11 | static void plot_points(config *cfg, plot_info *pi, data_set *ds); 12 | 13 | void free_rows(plot_info *pi); 14 | 15 | int ascii_plot(config *cfg, plot_info *pi, data_set *ds) { 16 | init_rows(pi); 17 | if (cfg->axis) { 18 | draw_calc_axis_pos(pi); 19 | draw_axes(pi); 20 | } 21 | 22 | print_header(pi, cfg->mode == MODE_COUNT, ds->columns); 23 | plot_points(cfg, pi, ds); 24 | 25 | for (size_t i = 0; i < pi->h; i++) { 26 | printf("%s\n", pi->rows[i]); 27 | } 28 | free_rows(pi); 29 | return 0; 30 | } 31 | 32 | void free_rows(plot_info *pi) { 33 | if (pi) { 34 | for (size_t i = 0; i < pi->h; i++) { 35 | free(pi->rows[i]); 36 | } 37 | free(pi->rows); 38 | pi->rows = NULL; 39 | } 40 | } 41 | 42 | static void init_rows(plot_info *pi) { 43 | char **rows = calloc(pi->h, sizeof(*rows)); 44 | if (rows == NULL) { err(1, "calloc"); } 45 | 46 | for (size_t i = 0; i < pi->h; i++) { 47 | rows[i] = malloc(pi->w + 1); 48 | if (rows[i] == NULL) { err(1, "malloc"); } 49 | memset(rows[i], ' ', pi->w); 50 | rows[i][pi->w] = '\0'; 51 | } 52 | pi->rows = rows; 53 | } 54 | 55 | static void print_header(plot_info *pi, bool point_counts, uint8_t columns) { 56 | if (pi->log_x) { 57 | printf(" x: log [%g - %g]", exp(pi->min_x), exp(pi->max_x)); 58 | } else { 59 | printf(" x: [%g - %g]", pi->min_x, pi->max_x); 60 | } 61 | 62 | if (pi->log_y) { 63 | printf(" y: log [%g - %g]", exp(pi->min_y), exp(pi->max_y)); 64 | } else { 65 | printf(" y: [%g - %g]", pi->min_y, pi->max_y); 66 | } 67 | 68 | if (!point_counts) { 69 | printf(" -- "); 70 | for (uint8_t i = 0; i < columns; i++) { 71 | printf("%s%d: %c", i > 0 ? ", " : "", i, col_mark(i)); 72 | } 73 | } 74 | printf("\n"); 75 | } 76 | 77 | static void draw_axes(plot_info *pi) { 78 | char c; 79 | 80 | /* TODO: use same step size calc as SVG */ 81 | 82 | for (size_t i = 0; i < pi->h; i++) { 83 | if (pi->draw_y_axis) { 84 | c = (i % 5 == 0 ? '+' : '|'); 85 | } else { 86 | c = (i % 5 == 0 ? '.' : ' '); 87 | } 88 | 89 | pi->rows[i][pi->axis_x] = c; 90 | } 91 | 92 | for (size_t i = 0; i < pi->w; i++) { 93 | if (pi->draw_x_axis) { 94 | c = (i % 5 == 0 ? '+' : '-'); 95 | } else { 96 | c = (i % 5 == 0 ? '.' : ' '); 97 | } 98 | pi->rows[pi->axis_y][i] = c; 99 | } 100 | 101 | pi->rows[pi->axis_y][pi->axis_x] = '+'; 102 | } 103 | 104 | static char col_marks[] = "#@*^!~%ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 105 | 106 | static char col_mark(uint8_t col) { 107 | if (col > sizeof(col_marks) / sizeof(col_marks[0])) { return '*'; } 108 | return col_marks[col]; 109 | } 110 | 111 | static void plot_points(config *cfg, plot_info *pi, data_set *ds) { 112 | for (uint8_t c = 0; c < ds->columns; c++) { 113 | for (size_t r = 0; r < ds->rows; r++) { 114 | point *p = &ds->pairs[c][r]; 115 | 116 | if (IS_EMPTY(p->x) || IS_EMPTY(p->y)) { continue; } 117 | transform_t t = scale_get_transform(pi->log_x, pi->log_y); 118 | scaled_point sp; 119 | scale_point(pi, p, &sp, t); 120 | LOG(2, "{ %g, %g } => [%u, %u]\n", p->x, p->y, sp.x, sp.y); 121 | 122 | char mark = col_mark(c); 123 | if (pi->counters) { 124 | size_t count = counter_get(pi->counters[c], sp.x, sp.y); 125 | if (count < 10) { 126 | mark = '0' + count; 127 | } else if (count < 36) { 128 | mark = 'a' + count - 10; 129 | } else { 130 | mark = '#'; 131 | } 132 | } 133 | pi->rows[sp.y][sp.x] = mark; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /draw.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "draw.h" 5 | #include "scale.h" 6 | 7 | #include "ascii.h" 8 | #include "svg.h" 9 | #include "counter.h" 10 | 11 | /* Common drawing functionality. */ 12 | 13 | static void count_points(counter *counter, plot_info *pi, data_set *ds, uint8_t column); 14 | static bool all_empty_points(plot_info *pi); 15 | static bool insufficient_range(plot_info *pi); 16 | 17 | static const double MAX = 1e100; // TODO: portable constants? DBL_MAX? 18 | static const double MIN = -1e100; 19 | 20 | int draw(config *cfg, data_set *ds) { 21 | plot_info pi; 22 | memset(&pi, 0, sizeof(pi)); 23 | 24 | pi.log_x = cfg->log_x; 25 | pi.log_y = cfg->log_y; 26 | 27 | draw_calc_bounds(ds, &pi); 28 | 29 | if (all_empty_points(&pi)) { return 0; } 30 | if (insufficient_range(&pi)) { return 0; } 31 | 32 | pi.w = cfg->width; 33 | pi.h = cfg->height; 34 | 35 | if (cfg->mode == MODE_COUNT) { 36 | pi.counters = calloc(ds->columns, sizeof(counter *)); 37 | assert(pi.counters); 38 | for (uint8_t c = 0; c < ds->columns; c++) { 39 | counter *counter = counter_init(ds->rows); 40 | assert(counter); 41 | count_points(counter, &pi, ds, c); 42 | pi.counters[c] = counter; 43 | } 44 | } 45 | 46 | int res = 0; 47 | 48 | switch (cfg->plot_type) { 49 | case PLOT_ASCII: 50 | res = ascii_plot(cfg, &pi, ds); 51 | break; 52 | 53 | case PLOT_SVG: 54 | res = svg_plot(cfg, &pi, ds); 55 | break; 56 | 57 | default: 58 | assert(false); 59 | break; 60 | } 61 | 62 | if (pi.counters) { 63 | for (uint8_t c = 0; c < ds->columns; c++) { 64 | counter_free(pi.counters[c]); 65 | } 66 | free(pi.counters); 67 | } 68 | return res; 69 | } 70 | 71 | static bool all_empty_points(plot_info *pi) { 72 | return (pi->min_x == MAX || pi->min_y == MAX); 73 | } 74 | 75 | static bool insufficient_range(plot_info *pi) { 76 | return (pi->range_x == 0) || (pi->range_y == 0); 77 | } 78 | 79 | void draw_calc_bounds(data_set *ds, plot_info *pi) { 80 | point min_p = { .x = MAX, .y = MAX }; 81 | point max_p = { .x = MIN, .y = MIN }; 82 | 83 | for (uint8_t c = 0; c < ds->columns; c++) { 84 | for (size_t r = 0; r < ds->rows; r++) { 85 | point *p = &ds->pairs[c][r]; 86 | 87 | if (IS_EMPTY_POINT(p)) { continue; } 88 | 89 | if (pi->log_x && p->x <= 0) { 90 | fprintf(stderr, "floating point error: log(%g)\n", p->x); 91 | exit(1); 92 | } 93 | if (pi->log_y && p->y <= 0) { 94 | fprintf(stderr, "floating point error: log(%g)\n", p->y); 95 | exit(1); 96 | } 97 | 98 | double x = p->x; 99 | double y = p->y; 100 | 101 | if (x < min_p.x) { min_p.x = x; } 102 | if (x > max_p.x) { max_p.x = x; } 103 | 104 | if (y < min_p.y) { min_p.y = y; } 105 | if (y > max_p.y) { max_p.y = y; } 106 | } 107 | } 108 | 109 | transform_t t = scale_get_transform(pi->log_x, pi->log_y); 110 | point out_min_p, out_max_p; 111 | scale_transform(&min_p, t, &out_min_p); 112 | scale_transform(&max_p, t, &out_max_p); 113 | 114 | pi->min_x = out_min_p.x; 115 | pi->min_y = out_min_p.y; 116 | pi->max_x = out_max_p.x; 117 | pi->max_y = out_max_p.y; 118 | 119 | /* Override bounds that would lead to a range of zero, to avoid a 120 | * crash when plotting. (Found by afl.) */ 121 | if (pi->min_x == pi->max_x) { pi->max_x = pi->min_x + 1; } 122 | if (pi->min_y == pi->max_y) { pi->max_y = pi->min_y + 1; } 123 | 124 | pi->range_x = pi->max_x - pi->min_x; 125 | pi->range_y = pi->max_y - pi->min_y; 126 | 127 | bool crosses_x = pi->min_x <= 0 && pi->max_x >= 0; 128 | bool crosses_y = pi->min_y <= 0 && pi->max_y >= 0; 129 | 130 | double cross_pad = CROSS_PAD; 131 | 132 | if (!crosses_x) { 133 | if (0 < pi->min_x && 0 > pi->min_x - pi->range_x*cross_pad) { 134 | pi->min_x = 0; 135 | pi->range_x = pi->max_x; 136 | } else if (0 > pi->max_x && 0 < pi->max_x + pi->range_x*cross_pad) { 137 | pi->max_x = 0; 138 | pi->range_x = -pi->min_x; 139 | } 140 | } 141 | 142 | if (!crosses_y) { 143 | if (0 < pi->min_y && 0 > pi->min_y - pi->range_y*cross_pad) { 144 | pi->min_y = 0; 145 | pi->range_y = pi->max_y; 146 | } else if (0 > pi->max_y && 0 < pi->max_y + pi->range_y*cross_pad) { 147 | pi->max_y = 0; 148 | pi->range_y = -pi->min_y; 149 | } 150 | } 151 | 152 | LOG(1, "mx %g, Mx %g, my %g, My %g\n", 153 | pi->min_x, pi->max_x, pi->min_y, pi->max_y); 154 | } 155 | 156 | void draw_calc_axis_pos(plot_info *pi) { 157 | pi->draw_x_axis = (0 >= pi->min_y && 0 <= pi->max_y); 158 | pi->draw_y_axis = (0 >= pi->min_x && 0 <= pi->max_x); 159 | 160 | point origin = { .x = 0, .y = 0 }; 161 | if (!pi->draw_y_axis) { 162 | if (0 < pi->min_x) { 163 | origin.x = pi->min_x; 164 | } else { 165 | origin.x = pi->max_x; 166 | } 167 | } 168 | 169 | if (!pi->draw_x_axis) { 170 | if (0 < pi->min_y) { 171 | origin.y = pi->min_y; 172 | } else { 173 | origin.y = pi->max_y; 174 | } 175 | } 176 | 177 | scaled_point sp; 178 | scale_point(pi, &origin, &sp, scale_get_transform(pi->log_x, pi->log_y)); 179 | pi->axis_x = sp.x; 180 | pi->axis_y = sp.y; 181 | LOG(1, "axis at: (%g, %g) scaled to (%d, %d)\n", origin.x, origin.y, sp.x, sp.y); 182 | } 183 | 184 | static void count_points(counter *counter, plot_info *pi, data_set *ds, uint8_t column) { 185 | point *points = ds->pairs[column]; 186 | transform_t t = scale_get_transform(pi->log_x, pi->log_y); 187 | 188 | for (size_t r = 0; r < ds->rows; r++) { 189 | point *p = &points[r]; 190 | if (IS_EMPTY(p->x) || IS_EMPTY(p->y)) { continue; } 191 | scaled_point sp; 192 | scale_point(pi, p, &sp, t); 193 | 194 | counter_increment(counter, sp.x, sp.y); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # guff: a plot device 2 | 3 | guff reads a stream of points from a file / stdin and plots them. 4 | 5 | guff is short for [MacGuffin][M]. 6 | 7 | [M]: https://en.wikipedia.org/wiki/MacGuffin 8 | 9 | guff is distributed under the [ISC License][ISC]. 10 | 11 | [ISC]: https://opensource.org/licenses/isc-license.txt 12 | 13 | ## Plots them? 14 | 15 | It can plot to stdout: 16 | 17 | $ wc -l *.c | grep -v total | sort -nr | awk '{print($1)}' | ./guff -d 72x30 18 | x: [0 - 15] y: [0 - 302] -- 0: # 19 | + 20 | # 21 | | 22 | | 23 | | 24 | + 25 | | # 26 | | 27 | | # 28 | | 29 | + # 30 | | # 31 | | # 32 | | # 33 | | 34 | + # 35 | | 36 | | # 37 | | 38 | | 39 | + 40 | | 41 | | # # 42 | | # 43 | | # 44 | + 45 | | # 46 | | 47 | | # # 48 | +----+----+----+----+----+----+----+----+----+----+----+----+----+----+- 49 | 50 | 51 | $ ./guff -d 40x20 -m count test_sin 52 | x: [0 - 720] y: [-1 - 1] 53 | + 54 | | ijjh 55 | | 9j1 2j8 56 | | ba ba 57 | | 98 98 58 | + 6a b5 59 | | 1d e 60 | | e e 61 | | 94 58 62 | | 2a a2 63 | 6----+----+----+---d+----+----+----+--8- 64 | 49 94 65 | |a3 49 66 | | e e 67 | | 2c d1 68 | + 78 96 69 | | b6 7a 70 | | d7 8c 71 | | cg hb 72 | | 3jjjj2 73 | 74 | Or to SVG: 75 | 76 | $ wc -l *.c | grep -v total | sort -nr | awk '{print($1)}' | ./guff -s -m line -r > example.svg 77 | 78 | ![](example.png) 79 | 80 | cat <range_x = pi->max_x - pi->min_x; 49 | pi->range_y = pi->max_y - pi->min_y; 50 | 51 | if (pi->w == 0) { pi->w = 72; } 52 | if (pi->h == 0) { pi->h = 40; } 53 | 54 | return scale_get_transform(pi->log_x, pi->log_y); 55 | } 56 | 57 | DEF_TEST(scale_basic) { 58 | plot_info pi = { 59 | .min_x = 0, 60 | .max_x = 100, 61 | .min_y = 0, 62 | .max_y = 100, 63 | }; 64 | transform_t t = init_pi(&pi); 65 | point p = { .x = 0, .y = 0 }; 66 | 67 | scaled_point sp; 68 | scale_point(&pi, &p, &sp, t); 69 | 70 | #define ASSERT_SCALED_TO(SP, X, Y) \ 71 | ASSERT_EQ_FMT((X), SP.x, "%d"); \ 72 | ASSERT_EQ_FMT((Y), SP.y, "%d") 73 | 74 | ASSERT_SCALED_TO(sp, 0, pi.h - 1); 75 | PASS(); 76 | } 77 | 78 | DEF_TEST(scale_centered_origin) { 79 | plot_info pi = { 80 | .min_x = -100, 81 | .max_x = 100, 82 | .min_y = -100, 83 | .max_y = 100, 84 | }; 85 | transform_t t = init_pi(&pi); 86 | point p = { .x = 0, .y = 0 }; 87 | 88 | scaled_point sp; 89 | scale_point(&pi, &p, &sp, t); 90 | 91 | ASSERT_SCALED_TO(sp, pi.w/2 - 1, pi.h/2); 92 | PASS(); 93 | } 94 | 95 | DEF_TEST(scale_example_regression) { 96 | plot_info pi = { 97 | .min_x = 1000, 98 | .max_x = 1040, 99 | .min_y = 1000, 100 | .max_y = 1080, 101 | 102 | .w = 640, 103 | .h = 480, 104 | }; 105 | transform_t t = init_pi(&pi); 106 | point p0 = { .x = 1000, .y = 992 }; 107 | point p1 = { .x = 1040, .y = 1066 }; 108 | 109 | scaled_point sp; 110 | scale_point(&pi, &p0, &sp, t); 111 | ASSERT_SCALED_TO(sp, 0, 526); 112 | 113 | scale_point(&pi, &p1, &sp, t); 114 | ASSERT_SCALED_TO(sp, 638, 85); 115 | PASS(); 116 | } 117 | 118 | DEF_TEST(scale_points) { 119 | plot_info pi = { 120 | .min_x = -100, 121 | .max_x = 100, 122 | .min_y = -100, 123 | .max_y = 100, 124 | }; 125 | transform_t t = init_pi(&pi); 126 | point p0 = { .x = 10, .y = 20 }; 127 | point p1 = { .x = 50, .y = 50 }; 128 | point p2 = { .x = -25, .y = -10 }; 129 | point p3 = { .x = 15, .y = 20 }; 130 | point p4 = { .x = -7, .y = 8 }; 131 | 132 | scaled_point sp; 133 | 134 | scale_point(&pi, &p0, &sp, t); 135 | ASSERT_SCALED_TO(sp, 38, 16); 136 | 137 | scale_point(&pi, &p1, &sp, t); 138 | ASSERT_SCALED_TO(sp, 52, 11); 139 | 140 | scale_point(&pi, &p2, &sp, t); 141 | ASSERT_SCALED_TO(sp, 26, 22); 142 | 143 | scale_point(&pi, &p3, &sp, t); 144 | ASSERT_SCALED_TO(sp, 40, 16); 145 | 146 | scale_point(&pi, &p4, &sp, t); 147 | ASSERT_SCALED_TO(sp, 33, 19); 148 | 149 | PASS(); 150 | } 151 | 152 | DEF_TEST(scale_basic_log) { 153 | plot_info pi = { 154 | .min_x = 0, 155 | .max_x = log(100), 156 | .min_y = 0, 157 | .max_y = log(1000), 158 | 159 | .log_x = true, 160 | .log_y = true, 161 | }; 162 | transform_t t = init_pi(&pi); 163 | point p = { .x = 50, .y = 100 }; 164 | 165 | scaled_point sp; 166 | scale_point(&pi, &p, &sp, t); 167 | ASSERT_SCALED_TO(sp, 59, 14); 168 | 169 | PASS(); 170 | } 171 | 172 | DEF_TEST(scale_points_log) { 173 | plot_info pi = { 174 | .min_x = -100, 175 | .max_x = 100, 176 | .min_y = 0, 177 | .max_y = log(100), 178 | .log_y = true, 179 | }; 180 | transform_t t = init_pi(&pi); 181 | point p0 = { .x = 10, .y = 20 }; 182 | point p1 = { .x = 50, .y = 50 }; 183 | point p2 = { .x = -25, .y = 1 }; 184 | point p3 = { .x = 15, .y = 20 }; 185 | point p4 = { .x = -7, .y = 8 }; 186 | 187 | scaled_point sp; 188 | 189 | scale_point(&pi, &p0, &sp, t); 190 | ASSERT_SCALED_TO(sp, 38, 14); 191 | 192 | scale_point(&pi, &p1, &sp, t); 193 | ASSERT_SCALED_TO(sp, 52, 7); 194 | 195 | scale_point(&pi, &p2, &sp, t); 196 | ASSERT_SCALED_TO(sp, 26, 39); 197 | 198 | scale_point(&pi, &p3, &sp, t); 199 | ASSERT_SCALED_TO(sp, 40, 14); 200 | 201 | scale_point(&pi, &p4, &sp, t); 202 | ASSERT_SCALED_TO(sp, 33, 22); 203 | 204 | PASS(); 205 | } 206 | 207 | DEF_TEST(scale_out_of_range) { 208 | plot_info pi = { 209 | .min_x = 1000, 210 | .max_x = 1040, 211 | .min_y = 1850, 212 | .max_y = 1925, 213 | 214 | .w = 640, 215 | .h = 480, 216 | }; 217 | transform_t t = init_pi(&pi); 218 | point p0 = { .x = 1000, .y = 1850 }; 219 | point p1 = { .x = 1040, .y = 1924 }; 220 | 221 | scaled_point sp; 222 | 223 | scale_point(&pi, &p0, &sp, t); 224 | ASSERT_SCALED_TO(sp, 0, 479); 225 | 226 | scale_point(&pi, &p1, &sp, t); 227 | ASSERT_SCALED_TO(sp, 638, 7); 228 | 229 | PASS(); 230 | } 231 | 232 | SUITE(s_scale) { 233 | SET_SETUP(setup_cb, NULL); 234 | SET_TEARDOWN(teardown_cb, NULL); 235 | 236 | RUN_TEST(get_transform); 237 | RUN_TEST(transform); 238 | 239 | RUN_TEST(scale_basic); 240 | RUN_TEST(scale_centered_origin); 241 | RUN_TEST(scale_example_regression); 242 | RUN_TEST(scale_points); 243 | RUN_TEST(scale_basic_log); 244 | RUN_TEST(scale_points_log); 245 | 246 | RUN_TEST(scale_out_of_range); 247 | } 248 | -------------------------------------------------------------------------------- /test_draw.c: -------------------------------------------------------------------------------- 1 | #include "test_guff.h" 2 | 3 | #include "draw.h" 4 | #include "input.h" 5 | #include "input_internal.h" 6 | 7 | static data_set ds; 8 | static plot_info pi; 9 | 10 | void free_rows(plot_info *pi); 11 | 12 | static void setup_cb(void *data) { 13 | memset(&pi, 0, sizeof(pi)); 14 | memset(&ds, 0, sizeof(ds)); 15 | } 16 | 17 | static void teardown_cb(void *data) { 18 | free_rows(&pi); 19 | input_free(&ds); 20 | } 21 | 22 | #define READ_LINES_AND_INIT_PI(CFG, LINES) \ 23 | do { \ 24 | if (CFG.log_x) { pi.log_x = true; } \ 25 | if (CFG.log_y) { pi.log_y = true; } \ 26 | size_t count = sizeof(LINES)/sizeof(LINES[0]); \ 27 | for (size_t i = 0; i < count; i++) { \ 28 | char *line = LINES[i]; \ 29 | ASSERT_EQ(SINK_LINE_OK, \ 30 | sink_line(&CFG, &ds, line, strlen(line), i)); \ 31 | } \ 32 | } while (0) \ 33 | 34 | DEF_TEST(bounds_basic) { 35 | config cfg; 36 | memset(&cfg, 0, sizeof(cfg)); 37 | char *lines[] = { 38 | "1 2 3", 39 | "4 5 6", 40 | "7 8 9", 41 | }; 42 | READ_LINES_AND_INIT_PI(cfg, lines); 43 | 44 | draw_calc_bounds(&ds, &pi); 45 | ASSERT_IN_RANGE(0, pi.min_x, 0.0001); 46 | ASSERT_IN_RANGE(2, pi.max_x, 0.0001); 47 | 48 | // 0, not 1 -> include axis 49 | ASSERT_IN_RANGE(0, pi.min_y, 0.0001); 50 | ASSERT_IN_RANGE(9, pi.max_y, 0.0001); 51 | 52 | PASS(); 53 | } 54 | 55 | DEF_TEST(bounds_empty_cells) { 56 | config cfg; 57 | memset(&cfg, 0, sizeof(cfg)); 58 | char *lines[] = { 59 | " 2 3", 60 | "4 6", 61 | "7 8 ", 62 | }; 63 | READ_LINES_AND_INIT_PI(cfg, lines); 64 | 65 | draw_calc_bounds(&ds, &pi); 66 | ASSERT_IN_RANGE(0, pi.min_x, 0.0001); 67 | ASSERT_IN_RANGE(2, pi.max_x, 0.0001); 68 | 69 | // 0, not 1 -> include axis 70 | ASSERT_IN_RANGE(0, pi.min_y, 0.0001); 71 | ASSERT_IN_RANGE(8, pi.max_y, 0.0001); 72 | 73 | PASS(); 74 | } 75 | 76 | DEF_TEST(bounds_negative_quadrant) { 77 | config cfg = { 78 | .x_column = true, 79 | }; 80 | char *lines[] = { 81 | "-5 -50", 82 | "-3 -30", 83 | "-1 -10", 84 | }; 85 | READ_LINES_AND_INIT_PI(cfg, lines); 86 | 87 | draw_calc_bounds(&ds, &pi); 88 | ASSERT_IN_RANGE(-5, pi.min_x, 0.0001); 89 | ASSERT_IN_RANGE(0, pi.max_x, 0.0001); 90 | 91 | ASSERT_IN_RANGE(-50, pi.min_y, 0.0001); 92 | ASSERT_IN_RANGE(0, pi.max_y, 0.0001); 93 | 94 | PASS(); 95 | } 96 | 97 | DEF_TEST(bounds_4q) { 98 | config cfg = { 99 | .x_column = true, 100 | }; 101 | char *lines[] = { 102 | "-50 -50", 103 | "-49 49", 104 | "0 0", 105 | "10 10", 106 | }; 107 | READ_LINES_AND_INIT_PI(cfg, lines); 108 | 109 | draw_calc_bounds(&ds, &pi); 110 | ASSERT_IN_RANGE(-50, pi.min_x, 0.0001); 111 | ASSERT_IN_RANGE(10, pi.max_x, 0.0001); 112 | 113 | ASSERT_IN_RANGE(-50, pi.min_y, 0.0001); 114 | ASSERT_IN_RANGE(49, pi.max_y, 0.0001); 115 | 116 | PASS(); 117 | } 118 | 119 | DEF_TEST(bounds_diff_quadrants_for_columns) { 120 | config cfg = { 121 | .x_column = true, 122 | }; 123 | char *lines[] = { 124 | "-50 -50 50", 125 | "-49 -49 49", 126 | "-48 -48 48", 127 | "-47 -47 47", 128 | }; 129 | READ_LINES_AND_INIT_PI(cfg, lines); 130 | 131 | draw_calc_bounds(&ds, &pi); 132 | ASSERT_IN_RANGE(-50, pi.min_x, 0.0001); 133 | ASSERT_IN_RANGE(-47, pi.max_x, 0.0001); 134 | 135 | ASSERT_IN_RANGE(-50, pi.min_y, 0.0001); 136 | ASSERT_IN_RANGE(50, pi.max_y, 0.0001); 137 | 138 | PASS(); 139 | } 140 | 141 | DEF_TEST(bounds_too_distant_to_touch_axis) { 142 | config cfg = { 143 | .x_column = true, 144 | }; 145 | char *lines[] = { 146 | "-5000 -5000", 147 | "-4900 -4900", 148 | "-4800 -4800", 149 | }; 150 | READ_LINES_AND_INIT_PI(cfg, lines); 151 | 152 | draw_calc_bounds(&ds, &pi); 153 | ASSERT_IN_RANGE(-5000, pi.min_x, 0.0001); 154 | ASSERT_IN_RANGE(-4800, pi.max_x, 0.0001); 155 | 156 | ASSERT_IN_RANGE(-5000, pi.min_y, 0.0001); 157 | ASSERT_IN_RANGE(-4800, pi.max_y, 0.0001); 158 | 159 | PASS(); 160 | } 161 | 162 | DEF_TEST(bounds_log_basic) { 163 | config cfg = { 164 | .log_y = true, 165 | }; 166 | char *lines[] = { 167 | "1214", 168 | "358", 169 | "316", 170 | "187", 171 | "186", 172 | "93", 173 | "63", 174 | "11", 175 | }; 176 | READ_LINES_AND_INIT_PI(cfg, lines); 177 | 178 | draw_calc_bounds(&ds, &pi); 179 | ASSERT_IN_RANGE(0, pi.min_x, 0.0001); 180 | ASSERT_IN_RANGE(7, pi.max_x, 0.0001); 181 | 182 | ASSERT_IN_RANGE(0, pi.min_y, 0.0001); 183 | ASSERT_IN_RANGE(/* log(1214) */ 7.101, pi.max_y, 0.001); 184 | PASS(); 185 | } 186 | 187 | DEF_TEST(bounds_log_distant) { 188 | config cfg = { 189 | .log_y = true, 190 | }; 191 | char *lines[] = { 192 | "1214", 193 | "358", 194 | "316", 195 | "187", 196 | "186", 197 | }; 198 | READ_LINES_AND_INIT_PI(cfg, lines); 199 | 200 | draw_calc_bounds(&ds, &pi); 201 | ASSERT_IN_RANGE(0, pi.min_x, 0.0001); 202 | ASSERT_IN_RANGE(4, pi.max_x, 0.0001); 203 | 204 | ASSERT_IN_RANGE(5.2257, pi.min_y, 0.0001); 205 | ASSERT_IN_RANGE(7.101, pi.max_y, 0.001); 206 | PASS(); 207 | } 208 | 209 | DEF_TEST(reject_x_range_of_zero) { 210 | config cfg; 211 | memset(&cfg, 0, sizeof(cfg)); 212 | char *lines[] = { 213 | "-3 -2", 214 | }; 215 | READ_LINES_AND_INIT_PI(cfg, lines); 216 | 217 | draw_calc_bounds(&ds, &pi); 218 | ASSERT(pi.range_x != 0); 219 | PASS(); 220 | } 221 | 222 | DEF_TEST(reject_y_range_of_zero) { 223 | config cfg; 224 | memset(&cfg, 0, sizeof(cfg)); 225 | char *lines[] = { 226 | "0 0", 227 | "0 0", 228 | }; 229 | READ_LINES_AND_INIT_PI(cfg, lines); 230 | 231 | draw_calc_bounds(&ds, &pi); 232 | ASSERT(pi.range_y != 0); 233 | PASS(); 234 | } 235 | 236 | SUITE(s_draw) { 237 | SET_SETUP(setup_cb, NULL); 238 | SET_TEARDOWN(teardown_cb, NULL); 239 | 240 | // calculating bounds / setting up plot info 241 | RUN_TEST(bounds_basic); 242 | RUN_TEST(bounds_empty_cells); 243 | RUN_TEST(bounds_negative_quadrant); 244 | RUN_TEST(bounds_4q); 245 | RUN_TEST(bounds_diff_quadrants_for_columns); 246 | RUN_TEST(bounds_too_distant_to_touch_axis); 247 | RUN_TEST(bounds_log_basic); 248 | RUN_TEST(bounds_log_distant); 249 | 250 | RUN_TEST(reject_x_range_of_zero); 251 | RUN_TEST(reject_y_range_of_zero); 252 | } 253 | -------------------------------------------------------------------------------- /input.c: -------------------------------------------------------------------------------- 1 | #include "input.h" 2 | #include "input_internal.h" 3 | 4 | /* Input handling. */ 5 | 6 | static void add_pair(config *cfg, data_set *ds, size_t row, uint8_t col, point *p); 7 | static bool number_head_char(char c); 8 | 9 | static char buf[64 * 1024]; 10 | 11 | int input_read(config *cfg, data_set *ds) { 12 | char *line = NULL; 13 | 14 | size_t row_count = 0; 15 | 16 | init_pairs(ds); 17 | 18 | while ((line = fgets(buf, sizeof(buf) - 1, cfg->in))) { 19 | size_t len = strlen(line); 20 | 21 | sink_line_res res = sink_line(cfg, ds, line, len, row_count); 22 | switch (res) { 23 | case SINK_LINE_OK: 24 | row_count++; 25 | break; 26 | case SINK_LINE_EMPTY: 27 | return (cfg->stream_mode ? 0 : -1); 28 | case SINK_LINE_COMMENT: 29 | continue; 30 | case SINK_LINE_DONE: 31 | return 0; 32 | 33 | default: 34 | assert(false); 35 | } 36 | } 37 | 38 | return -1; // end of stream 39 | } 40 | 41 | static bool is_comment_marker(char c) { 42 | switch (c) { 43 | case '#': case '/': 44 | return true; 45 | default: 46 | return false; 47 | } 48 | } 49 | 50 | sink_line_res sink_line(config *cfg, data_set *ds, char *line, size_t len, size_t row_count) { 51 | size_t col = 0; 52 | 53 | if (len == 0) { return SINK_LINE_EMPTY; } 54 | if (line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } 55 | if (*line == '\0') { return SINK_LINE_EMPTY; } 56 | LOG(3, "sink_line: %s\n", line); 57 | 58 | float cur_x = row_count; 59 | bool has_x = false; 60 | 61 | // ignore comments 62 | if (is_comment_marker(line[0])) { return SINK_LINE_COMMENT; } 63 | 64 | size_t offset = 0; 65 | while (offset < len && line[offset]) { 66 | double v = 0; 67 | if (offset >= len) { break; } 68 | 69 | // ignore comment to EOL 70 | if (is_comment_marker(line[offset])) { return SINK_LINE_OK; } 71 | 72 | if (!number_head_char(line[offset])) { 73 | if (offset == 0) { 74 | v = EMPTY_VALUE; 75 | } else { 76 | offset++; 77 | if (!number_head_char(line[offset])) { 78 | v = EMPTY_VALUE; 79 | } 80 | } 81 | } 82 | 83 | char *cur_line = &line[offset]; 84 | char *out_line = NULL; 85 | if (isnan(v)) { 86 | offset++; // already got the value 87 | } else { 88 | v = strtod(cur_line, &out_line); 89 | if (isinf(v)) { v = EMPTY_VALUE; } 90 | } 91 | if (out_line == cur_line) { 92 | break; 93 | } else { 94 | // could do this in terms of *line == '\0' or line[offset] == len. 95 | // strtod shifts &line. 96 | while(&line[offset] < out_line) { 97 | offset++; 98 | } 99 | } 100 | 101 | if (cfg->x_column && !has_x) { 102 | cur_x = v; 103 | has_x = true; 104 | } else { 105 | point p = { .x = cur_x, .y = v }; 106 | add_pair(cfg, ds, row_count, col, &p); 107 | col++; 108 | if (col == MAX_COLUMNS) { break; } 109 | } 110 | } 111 | 112 | // Fill remaining columns with EMPTY_VALUE 113 | for (size_t c = col; c < ds->columns; c++) { 114 | point p = { .x = cur_x, .y = EMPTY_VALUE }; 115 | add_pair(cfg, ds, row_count, c, &p); 116 | } 117 | 118 | return SINK_LINE_OK; 119 | } 120 | 121 | /* Is c a character which can be at the start of a double literal? */ 122 | static bool number_head_char(char c) { 123 | switch (c) { 124 | case '0': case '1': case '2': case '3': case '4': 125 | case '5': case '6': case '7': case '8': case '9': 126 | case '-': case '+': case '.': case 'e': 127 | return true; 128 | default: 129 | return false; 130 | } 131 | } 132 | 133 | #define MIN_ROW_CEIL2 1 134 | 135 | void init_pairs(data_set *ds) { 136 | point **cols = calloc(1, sizeof(point *)); 137 | if (cols == NULL) { err(1, "calloc"); } 138 | 139 | cols[0] = calloc(1 << MIN_ROW_CEIL2, sizeof(point)); 140 | if (cols[0] == NULL) { err(1, "calloc"); } 141 | 142 | ds->row_ceil2 = MIN_ROW_CEIL2; 143 | ds->columns = 1; 144 | ds->pairs = cols; 145 | } 146 | 147 | static void add_pair(config *cfg, data_set *ds, size_t row, uint8_t col, point *p) { 148 | // add columns, padding out empty cells as necessary 149 | if (col >= ds->columns) { 150 | point **npairs = realloc(ds->pairs, (1 + col) * sizeof(*npairs)); 151 | LOG(2, "growing ds->pairs: %p(%u) => %p(%u), %zu bytes\n", 152 | ds->pairs, ds->columns, npairs, 1 + col, (1 + col) * sizeof(*npairs)); 153 | if (npairs == NULL) { err(1, "realloc"); } 154 | 155 | uint8_t row_ceil2 = ds->row_ceil2; 156 | if (row_ceil2 == 0) { 157 | ds->row_ceil2 = MIN_ROW_CEIL2; 158 | row_ceil2 = ds->row_ceil2; 159 | } 160 | 161 | point *column = calloc(1, (1 << row_ceil2) * sizeof(point)); 162 | if (column == NULL) { err(1, "calloc"); } 163 | npairs[col] = column; 164 | ds->pairs = npairs; 165 | ds->columns = col + 1; 166 | for (size_t i = 0; i < ds->rows; i++) { 167 | point np = { .x = i, .y = EMPTY_VALUE }; 168 | if (cfg->x_column) { 169 | assert(col > 0); 170 | np.x = npairs[col][0].x; 171 | } 172 | npairs[col][i] = np; 173 | } 174 | } 175 | 176 | // grow rows 177 | if (row >= (1 << ds->row_ceil2)) { 178 | uint8_t nceil2 = ds->row_ceil2; 179 | while ((1 << nceil2) <= row) { nceil2++; } 180 | 181 | if (nceil2 > ds->row_ceil2) { 182 | for (uint8_t c = 0; c < ds->columns; c++) { 183 | point *ncol = realloc(ds->pairs[c], (1 << nceil2) * sizeof(point)); 184 | LOG(2, "growing column %u, %p (%d) => %p (%d, %zu bytes)\n", 185 | c, ds->pairs[c], 1 << ds->row_ceil2, 186 | ncol, 1 << nceil2, (1 << nceil2) * sizeof(point)); 187 | if (ncol == NULL) { err(1, "realloc"); } 188 | ds->pairs[c] = ncol; 189 | } 190 | ds->row_ceil2 = nceil2; 191 | } 192 | } 193 | 194 | if (row >= ds->rows) { ds->rows = row + 1; } 195 | 196 | assert(ds->columns > col); 197 | assert(ds->rows > row); 198 | 199 | if (cfg->flip_xy) { 200 | ds->pairs[col][row] = (point){ .x = p->y, .y = p->x }; 201 | } else { 202 | ds->pairs[col][row] = *p; 203 | } 204 | LOG(2, "-- set [c:%u,r:%zu] to (%g, %g)\n", 205 | col, row, ds->pairs[col][row].x, ds->pairs[col][row].y); 206 | } 207 | 208 | void input_free(data_set *ds) { 209 | if (ds && ds->pairs) { 210 | point **pairs = ds->pairs; 211 | for (uint8_t c = 0; c < ds->columns; c++) { 212 | free(pairs[c]); 213 | pairs[c] = NULL; 214 | } 215 | free(pairs); 216 | memset(ds, 0, sizeof(*ds)); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /args.c: -------------------------------------------------------------------------------- 1 | #include "args.h" 2 | 3 | #include 4 | 5 | #include "svg.h" 6 | 7 | /* CLI argument handling. */ 8 | 9 | static void init_ascii(config *cfg); 10 | static void init_svg(config *cfg); 11 | 12 | static void usage(const char *msg) { 13 | if (msg) { fprintf(stderr, "%s\n\n", msg); } 14 | fprintf(stderr, "guff v. %d.%d.%d by %s\n", 15 | GUFF_VERSION_MAJOR, GUFF_VERSION_MINOR, 16 | GUFF_VERSION_PATCH, GUFF_AUTHOR); 17 | fprintf(stderr, 18 | "\n" 19 | "Usage: guff [-A] [-c] [-d WxH] [-f] [-h] [-l xyc]\n" 20 | " [-m MODE] [-r] [-s] [-S] [-x] [FILE]\n" 21 | "\n" 22 | "Common options:\n" 23 | " -d WxH: set width and height (e.g. \"-d 72x40\", \"-d 640x480\")\n" 24 | " -f: flip x & y axes in plot\n" 25 | " -h: print this message\n" 26 | " -l LOG: any of 'x', 'y', 'c' -- set X, Y, and/or count to log scale\n" 27 | " -m MODE: dot, count, line (SVG only), default dot\n" 28 | " -s: render to SVG\n" 29 | " -x: treat first column as X for all following Y columns (def: use row count)\n" 30 | "\n" 31 | "SVG only:\n" 32 | " -c: use colorblind-safe default colors\n" 33 | " -r: draw linear regression lines\n" 34 | "\n" 35 | "Other options:\n" 36 | " -A: don't draw axes\n" 37 | " -S: disable stream mode\n" 38 | ); 39 | exit(1); 40 | } 41 | 42 | static void parse_dims(config *cfg, const char *opt) { 43 | char *x = strchr(opt, 'x'); 44 | if (x) { 45 | cfg->width = atoi(opt); 46 | x++; 47 | cfg->height = atoi(x); 48 | } else { 49 | usage("Bad -d argument, should be formatted like -d 72x40"); 50 | } 51 | } 52 | 53 | void args_handle(config *cfg, int argc, char **argv) { 54 | int fl; 55 | while ((fl = getopt(argc, argv, "Acd:fhl:m:rsSx")) != -1) { 56 | switch (fl) { 57 | case 'A': /* no axis */ 58 | cfg->axis = false; 59 | break; 60 | case 'c': /* use colorblind-safe default colors */ 61 | cfg->colorblind = true; 62 | break; 63 | case 'd': /* dimensions */ 64 | parse_dims(cfg, optarg); 65 | break; 66 | case 'f': /* flip x/y */ 67 | cfg->flip_xy = true; 68 | break; 69 | case 'h': /* help */ 70 | usage(NULL); 71 | break; 72 | case 'l': /* log */ 73 | if (strchr(optarg, 'x')) { cfg->log_x = true; } 74 | if (strchr(optarg, 'y')) { cfg->log_y = true; } 75 | if (strchr(optarg, 'c')) { cfg->log_count = true; } 76 | break; 77 | case 'm': /* mode */ 78 | switch (optarg[0]) { 79 | case 'c': 80 | cfg->mode = MODE_COUNT; 81 | break; 82 | case 'd': 83 | cfg->mode = MODE_DOT; 84 | break; 85 | case 'l': 86 | cfg->mode = MODE_LINE; 87 | break; 88 | default: 89 | usage("Bad argument to -m: must be 'count', 'dot', or 'line'."); 90 | } 91 | break; 92 | case 'r': /* linear regression */ 93 | cfg->regression = true; 94 | break; 95 | case 's': /* SVG */ 96 | cfg->plot_type = PLOT_SVG; 97 | break; 98 | case 'S': /* disable stream mode */ 99 | cfg->stream_mode = false; 100 | break; 101 | case 'x': /* col 0 is X value */ 102 | cfg->x_column = true; 103 | break; 104 | case '?': 105 | default: 106 | usage(NULL); 107 | } 108 | } 109 | 110 | argc -= (optind - 1); 111 | argv += (optind - 1); 112 | if (argc > 1) { 113 | cfg->in_path = argv[1]; 114 | if (0 != strcmp("-", cfg->in_path)) { 115 | cfg->in = fopen(cfg->in_path, "r"); 116 | if (cfg->in == NULL) { err(1, "fopen"); } 117 | } 118 | } 119 | 120 | if (cfg->plot_type == PLOT_SVG) { 121 | init_svg(cfg); 122 | } else { 123 | init_ascii(cfg); 124 | } 125 | } 126 | 127 | static void init_ascii(config *cfg) { 128 | if (cfg->width == 0) { cfg->width = 72; } 129 | if (cfg->height == 0) { cfg->height = 40; } 130 | } 131 | 132 | static char *read_env_var(char *name) { 133 | char *v = getenv(name); 134 | if (v) { 135 | char *quote = strchr(v, '"'); 136 | if (quote) { *quote = '\0'; } 137 | } 138 | return v; 139 | } 140 | 141 | /* Colors chosen using http://colorbrewer2.org/ , 142 | * qualitative color scheme, 9 data classes, not colorblind-safe. */ 143 | static char *default_colors[] = { 144 | "#377eb8", 145 | "#e41a1c", 146 | "#4daf4a", 147 | "#984ea3", 148 | "#ff7f00", 149 | "#ffff33", 150 | "#a65628", 151 | "#f781bf", 152 | "#999999", 153 | }; 154 | 155 | /* Colors chosen using http://colorbrewer2.org/ , 156 | * diverging color scheme, 9 data classes, colorblind-safe. */ 157 | static char *default_colorblind_safe[] = { 158 | "#762a83", 159 | "#9970ab", 160 | "#c2a5cf", 161 | "#e7d4e8", 162 | "#f7f7f7", 163 | "#d9f0d3", 164 | "#a6dba0", 165 | "#5aae61", 166 | "#1b7837", 167 | }; 168 | 169 | static void init_svg(config *cfg) { 170 | svg_theme *theme = calloc(1, sizeof(*theme)); 171 | assert(theme); 172 | if (cfg->width == 0) { cfg->width = 320; } 173 | if (cfg->height == 0) { cfg->height = 200; } 174 | 175 | 176 | #define DEF_STR_OPTION(VAR, ENV_VAR, DEFAULT) \ 177 | do { \ 178 | theme->VAR = read_env_var("GUFF_" ENV_VAR); \ 179 | if (theme->VAR == NULL) { theme->VAR = DEFAULT; } \ 180 | } while (0) 181 | 182 | #define DEF_INT_OPTION(VAR, ENV_VAR, DEFAULT) \ 183 | do { \ 184 | char *var = read_env_var("GUFF_" ENV_VAR); \ 185 | if (var == NULL) { var = DEFAULT; } \ 186 | theme->VAR = atol(var); \ 187 | } while (0) 188 | 189 | DEF_STR_OPTION(bg_color, "BG_COLOR", "black"); 190 | DEF_STR_OPTION(border_color, "BORDER_COLOR", "black"); 191 | DEF_STR_OPTION(axis_color, "AXIS_COLOR", "lightgray"); 192 | 193 | char **palette = cfg->colorblind ? default_colorblind_safe : default_colors; 194 | 195 | DEF_STR_OPTION(colors[0], "COLOR0", palette[0]); 196 | DEF_STR_OPTION(colors[1], "COLOR1", palette[1]); 197 | DEF_STR_OPTION(colors[2], "COLOR2", palette[2]); 198 | DEF_STR_OPTION(colors[3], "COLOR3", palette[3]); 199 | DEF_STR_OPTION(colors[4], "COLOR4", palette[4]); 200 | DEF_STR_OPTION(colors[5], "COLOR5", palette[5]); 201 | DEF_STR_OPTION(colors[6], "COLOR6", palette[6]); 202 | DEF_STR_OPTION(colors[7], "COLOR7", palette[7]); 203 | DEF_STR_OPTION(colors[8], "COLOR8", palette[8]); 204 | 205 | DEF_INT_OPTION(border_width, "BORDER_WIDTH", "2"); 206 | DEF_INT_OPTION(line_width, "LINE_WIDTH", "2"); 207 | DEF_INT_OPTION(axis_width, "AXIS_WIDTH", "2"); 208 | 209 | cfg->svg_theme = theme; 210 | } 211 | -------------------------------------------------------------------------------- /man/guff.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | guff(1) - a plot device 7 | 44 | 45 | 52 | 53 |
54 | 55 | 66 | 67 |
    68 |
  1. guff(1)
  2. 69 |
  3. 70 |
  4. guff(1)
  5. 71 |
72 | 73 |

NAME

74 |

75 | guff - a plot device 76 |

77 | 78 |

SYNOPSIS

79 | 80 |

guff [-A] [-c] [-d WxH] [-f] [-h] [-l xyc] 81 | [-m MODE] [-r] [-s] [-S] [-x] [FILE]

82 | 83 |

DESCRIPTION

84 | 85 |

guff reads a stream of points from a file / stdin and plots them.

86 | 87 |

OPTIONS

88 | 89 |

Common options:

90 | 91 |
92 |
-d WxH

Set the dimensions (width and height). Should be formatted 93 | like "-d WxH", e.g. "-d 72x40" or "-d 640x480".

94 |
-f

Flip X and Y axes in plot.

95 |
-h

Print a help message.

96 |
-l xyc

Set X, Y, and/or Count to log-scale.

97 |
-m MODE

Set mode to dot (default), line (SVG only), or count (which 98 | tracks how densely clustered points are).

99 |
-x

Treat the first column as the X value for the other columns. 100 | Otherwise, the row number is used for the X value.

101 |
102 | 103 | 104 |

SVG-only options:

105 | 106 |
107 |
-c

Use colorblind-safe default colors.

108 |
-r

Draw a linear regression line for each column.

109 |
110 | 111 | 112 |

Rare options:

113 | 114 |
115 |
-A

Don't draw axes.

116 |
-S

Disable stream mode (exit at first blank line).

117 |
118 | 119 | 120 |

EXIT STATUS

121 | 122 |

Returns 0.

123 | 124 |

EXAMPLES

125 | 126 |

Read a series of number rows on stdin, and plot to ASCII once 127 | end-of-stream or a blank line is reached:

128 | 129 |
$ guff
130 | 
131 | 132 |

Same, but generate SVG:

133 | 134 |
$ guff -s
135 | 
136 | 137 |

Read number rows from a file:

138 | 139 |
$ guff file
140 | 
141 | 142 |

Plot stdin with a log-scale on the Y axis:

143 | 144 |
$ guff -ly
145 | 
146 | 147 |

Treat the first value on each line as the X value for all columns:

148 | 149 |
$ guff -x
150 | 
151 | 152 |

Plot stdin to SVG, with lines connecting the points:

153 | 154 |
$ guff -s -m line
155 | 
156 | 157 |

Same, but using a colorblind-safe palette:

158 | 159 |
$ guff -s -c -m line
160 | 
161 | 162 |

Plot stdin to SVG, and draw a linear regression line for each column:

163 | 164 |
$ guff -s -r
165 | 
166 | 167 |

Plot stdin with point counts, to show point density:

168 | 169 |
$ guff -m count
170 | 
171 | 172 |

Same, with a log scale for the point counts:

173 | 174 |
$ guff -m count -lc
175 | 
176 | 177 |

Same, with SVG output:

178 | 179 |
$ guff -s -m count -lc
180 | 
181 | 182 |

BUGS

183 | 184 |

There isn't a way to change the plot options when a blank line resets 185 | the data.

186 | 187 |

Rather than attempt to intelligently handle strange input, guff just 188 | skips the rest of the line when strtod(3) indicates there isn't a 189 | well-formatted number.

190 | 191 | 192 | 193 |

guff is Copyright (C) 2015 Scott Vokes vokes.s@gmail.com.

194 | 195 |

SEE ALSO

196 | 197 |

awk(1)

198 | 199 | 200 |
    201 |
  1. 202 |
  2. July 2015
  3. 203 |
  4. guff(1)
  5. 204 |
205 | 206 |
207 | 208 | 209 | -------------------------------------------------------------------------------- /svg.c: -------------------------------------------------------------------------------- 1 | #include "svg.h" 2 | #include "regression.h" 3 | #include "scale.h" 4 | #include "counter.h" 5 | 6 | /* SVG generation. */ 7 | 8 | #define REGRESSION_LINE_WIDTH 2 9 | 10 | static char *get_color(uint8_t column, svg_theme *theme); 11 | static void svg_printf_header(size_t w, size_t h); 12 | static void svg_printf_frame(size_t w, size_t h, char *fill_color, size_t border_width, char *border_color); 13 | static void svg_printf_begin_polyline(void); 14 | static void svg_printf_polyline_point(size_t x, size_t y); 15 | static void svg_printf_end_polyline(char *color, size_t line_width); 16 | static void svg_printf_circle(size_t x, size_t y, size_t point_size, char *color); 17 | static void svg_printf_axis(plot_info *pi, svg_theme *theme); 18 | static void svg_printf_regression_line(plot_info *pi, char *color, double slope, double intercept); 19 | static void svg_printf_end(void); 20 | 21 | int svg_plot(config *cfg, plot_info *pi, data_set *ds) { 22 | svg_theme *theme = cfg->svg_theme; 23 | svg_printf_header(pi->w, pi->h); 24 | svg_printf_frame(pi->w, pi->h, theme->bg_color, theme->border_width, theme->border_color); 25 | 26 | if (cfg->axis) { 27 | draw_calc_axis_pos(pi); 28 | svg_printf_axis(pi, theme); 29 | } 30 | 31 | transform_t transform = scale_get_transform(pi->log_x, pi->log_y); 32 | 33 | for (uint8_t c = 0; c < ds->columns; c++) { 34 | char *color = get_color(c, theme); 35 | point *column = ds->pairs[c]; 36 | 37 | if (cfg->mode == MODE_LINE) { 38 | bool beginning_line = true; 39 | for (size_t i = 0; i < ds->rows; i++) { 40 | point *p = &column[i]; 41 | if (IS_EMPTY(p->x) || IS_EMPTY(p->y)) { 42 | if (!beginning_line) { 43 | svg_printf_end_polyline(color, theme->line_width); 44 | } 45 | beginning_line = true; 46 | continue; 47 | } 48 | 49 | if (beginning_line) { 50 | svg_printf_begin_polyline(); 51 | beginning_line = false; 52 | } 53 | scaled_point sp; 54 | scale_point(pi, p, &sp, transform); 55 | svg_printf_polyline_point(sp.x, sp.y); 56 | } 57 | svg_printf_end_polyline(color, theme->line_width); 58 | } else { 59 | for (size_t i = 0; i < ds->rows; i++) { 60 | point *p = &column[i]; 61 | scaled_point sp; 62 | scale_point(pi, p, &sp, transform); 63 | 64 | if (IS_EMPTY(p->x) || IS_EMPTY(p->y)) { continue; } 65 | size_t point_size = SVG_DEF_POINT_SIZE; 66 | if (pi->counters) { 67 | size_t count = counter_get(pi->counters[c], sp.x, sp.y); 68 | point_size = SVG_DEF_POINT_SIZE + (cfg->log_count ? log(count) : count); 69 | } 70 | svg_printf_circle(sp.x, sp.y, point_size, color); 71 | } 72 | } 73 | 74 | if (cfg->regression) { 75 | double slope = 0; 76 | double intercept = 0; 77 | 78 | regression(column, ds->rows, transform, &slope, &intercept); 79 | svg_printf_regression_line(pi, color, slope, intercept); 80 | } 81 | } 82 | 83 | svg_printf_end(); 84 | return 0; 85 | } 86 | 87 | static char *get_color(uint8_t column, svg_theme *theme) { 88 | if (column < SVG_COLOR_COUNT) { 89 | return theme->colors[column]; 90 | } else { 91 | return theme->colors[SVG_COLOR_COUNT - 1]; 92 | } 93 | } 94 | 95 | static void svg_printf_header(size_t w, size_t h) { 96 | printf("\n", 98 | w, h); 99 | printf("\n", 100 | GUFF_VERSION_MAJOR, GUFF_VERSION_MINOR, GUFF_VERSION_PATCH); 101 | } 102 | 103 | static void svg_printf_frame(size_t w, size_t h, char *fill_color, 104 | size_t border_width, char *border_color) { 105 | printf("\n", 108 | fill_color, border_width, border_color); 109 | } 110 | 111 | static void svg_printf_begin_polyline(void) { 112 | printf("\n", 121 | color, line_width); 122 | } 123 | 124 | static void svg_printf_circle(size_t x, size_t y, size_t point_size, char *color) { 125 | printf("\n", 126 | x, y, point_size, color); 127 | } 128 | 129 | static double scale_tick(size_t width, double range) { 130 | /* Return a size that divides the range to add roughly 5-10 ticks. */ 131 | double rounded = pow(10, ceil(log10(range))); 132 | double step = rounded / (range < rounded / 2 ? 20 : 10); 133 | return width * (step / range); 134 | } 135 | 136 | static void svg_printf_axis(plot_info *pi, svg_theme *theme) { 137 | int tick_w = 3*theme->axis_width; 138 | 139 | // Y axis 140 | printf("\n", 142 | pi->axis_x, 0, pi->axis_x, pi->h, 143 | theme->axis_color, theme->axis_width, pi->draw_y_axis ? "" : "stroke-dasharray=\"2,5\" "); 144 | 145 | // X axis ticks 146 | if (pi->draw_x_axis) { 147 | int y0 = pi->axis_y - tick_w; 148 | int y1 = pi->axis_y + tick_w; 149 | 150 | double xto = scale_tick(pi->w, pi->range_x); 151 | for (int wx = pi->axis_x + xto; wx < pi->w; wx += xto) { 152 | printf("\n", 154 | wx, y0, wx, y1, theme->axis_color); 155 | } 156 | for (int wx = pi->axis_x - xto; wx > 0; wx -= xto) { 157 | printf("\n", 159 | wx, y0, wx, y1, theme->axis_color); 160 | } 161 | } 162 | 163 | // X axis 164 | printf("\n", 166 | 0L, pi->axis_y, pi->w, pi->axis_y, 167 | theme->axis_color, theme->axis_width, pi->draw_x_axis ? "" : "stroke-dasharray=\"2,5\" "); 168 | 169 | // Y axis ticks 170 | if (pi->draw_y_axis) { 171 | int x0 = pi->axis_x - tick_w; 172 | int x1 = pi->axis_x + tick_w; 173 | if (x0 > pi->w) { x0 = 0; } // don't wrap 174 | 175 | double yto = scale_tick(pi->h, pi->range_y); 176 | for (int hy = pi->axis_y + yto; hy < pi->h; hy += yto) { 177 | printf("\n", 179 | x0, hy, x1, hy, theme->axis_color); 180 | } 181 | for (int hy = pi->axis_y - yto; hy > 0; hy -= yto) { 182 | printf("\n", 184 | x0, hy, x1, hy, theme->axis_color); 185 | } 186 | } 187 | } 188 | 189 | static void svg_printf_end(void) { 190 | printf("\n"); 191 | } 192 | 193 | static void svg_printf_regression_line(plot_info *pi, char *color, 194 | double slope, double intercept) { 195 | 196 | point p0 = { .x = pi->min_x, .y = slope * pi->min_x + intercept }; 197 | point p1 = { .x = pi->max_x, .y = slope * pi->max_x + intercept }; 198 | LOG(2, "p0: %g * %g + %g => %g\n", slope, pi->min_x, intercept, p0.y); 199 | LOG(2, "p1: %g * %g + %g => %g\n", slope, pi->max_x, intercept, p1.y); 200 | 201 | scaled_point sp0, sp1; 202 | transform_t t = TRANSFORM_NONE; // already transformed 203 | scale_point(pi, &p0, &sp0, t); 204 | scale_point(pi, &p1, &sp1, t); 205 | 206 | LOG(2, "p0: (%g, %g) => [%d, %d]\n", p0.x, p0.y, sp0.x, sp0.y); 207 | LOG(2, "p1: (%g, %g) => [%d, %d]\n", p1.x, p1.y, sp1.x, sp1.y); 208 | 209 | printf("\n", 211 | sp0.x, sp0.y, sp1.x, sp1.y, color, REGRESSION_LINE_WIDTH); 212 | } 213 | -------------------------------------------------------------------------------- /test_input.c: -------------------------------------------------------------------------------- 1 | #include "test_guff.h" 2 | 3 | #include "input.h" 4 | #include "input_internal.h" 5 | #include 6 | 7 | static data_set ds; 8 | static config empty_cfg; 9 | 10 | static void setup_cb(void *data) { 11 | memset(&ds, 0, sizeof(ds)); 12 | } 13 | 14 | static void teardown_cb(void *data) { 15 | input_free(&ds); 16 | } 17 | 18 | DEF_TEST(input_empty) { 19 | init_pairs(&ds); 20 | char empty[] = "\n"; 21 | ASSERT_EQ(SINK_LINE_EMPTY, sink_line(&empty_cfg, &ds, NULL, 0, 0)); 22 | ASSERT_EQ(SINK_LINE_EMPTY, sink_line(&empty_cfg, &ds, empty, 1, 0)); 23 | PASS(); 24 | } 25 | 26 | DEF_TEST(input_comment) { 27 | init_pairs(&ds); 28 | char c1[] = "#a comment"; 29 | char c2[] = "// a comment"; 30 | ASSERT_EQ(SINK_LINE_COMMENT, sink_line(&empty_cfg, &ds, c1, strlen(c1), 0)); 31 | ASSERT_EQ(SINK_LINE_COMMENT, sink_line(&empty_cfg, &ds, c2, strlen(c2), 0)); 32 | PASS(); 33 | } 34 | 35 | DEF_TEST(input_single) { 36 | init_pairs(&ds); 37 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "23", 2, 0)); 38 | ASSERT_EQ(1, ds.columns); 39 | 40 | point exp = { .x = 0, .y = 23 }; 41 | ASSERT_EQUAL_T(&exp, &ds.pairs[0][0], type_point, NULL); 42 | 43 | PASS(); 44 | } 45 | 46 | DEF_TEST(input_pair) { 47 | init_pairs(&ds); 48 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "23 24", 5, 0)); 49 | 50 | ASSERT_EQ(2, ds.columns); 51 | point exp0 = { .x = 0, .y = 23 }; 52 | point exp1 = { .x = 0, .y = 24 }; 53 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 54 | ASSERT_EQUAL_T(&exp1, &ds.pairs[1][0], type_point, NULL); 55 | 56 | PASS(); 57 | } 58 | 59 | DEF_TEST(input_floats) { 60 | init_pairs(&ds); 61 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "99.9 999.999", 13, 0)); 62 | 63 | ASSERT_EQ(2, ds.columns); 64 | point exp0 = { .x = 0, .y = 99.9 }; 65 | point exp1 = { .x = 0, .y = 999.999 }; 66 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 67 | ASSERT_EQUAL_T(&exp1, &ds.pairs[1][0], type_point, NULL); 68 | 69 | PASS(); 70 | } 71 | 72 | DEF_TEST(input_csv) { 73 | init_pairs(&ds); 74 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "23,24", 15, 0)); 75 | 76 | ASSERT_EQ(2, ds.columns); 77 | ASSERT_EQ(1, ds.rows); 78 | point exp0 = { .x = 0, .y = 23 }; 79 | point exp1 = { .x = 0, .y = 24 }; 80 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 81 | ASSERT_EQUAL_T(&exp1, &ds.pairs[1][0], type_point, NULL); 82 | 83 | PASS(); 84 | } 85 | 86 | DEF_TEST(input_tab) { 87 | init_pairs(&ds); 88 | 89 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "23\t24", 5, 0)); 90 | 91 | ASSERT_EQ(2, ds.columns); 92 | ASSERT_EQ(1, ds.rows); 93 | point exp0 = { .x = 0, .y = 23 }; 94 | point exp1 = { .x = 0, .y = 24 }; 95 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 96 | ASSERT_EQUAL_T(&exp1, &ds.pairs[1][0], type_point, NULL); 97 | 98 | PASS(); 99 | } 100 | 101 | DEF_TEST(input_exponent) { 102 | init_pairs(&ds); 103 | 104 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "-24e-7,+50e5", 16, 0)); 105 | 106 | ASSERT_EQ(2, ds.columns); 107 | ASSERT_EQ(1, ds.rows); 108 | point exp0 = { .x = 0, .y = -24e-7 }; 109 | point exp1 = { .x = 0, .y = 5e6 }; 110 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 111 | ASSERT_EQUAL_T(&exp1, &ds.pairs[1][0], type_point, NULL); 112 | 113 | PASS(); 114 | } 115 | 116 | DEF_TEST(input_multiline) { 117 | init_pairs(&ds); 118 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "23", 2, 0)); 119 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "24", 2, 1)); 120 | 121 | ASSERT_EQ(1, ds.columns); 122 | ASSERT_EQ(2, ds.rows); 123 | point exp0_0 = { .x = 0, .y = 23 }; 124 | point exp1_0 = { .x = 1, .y = 24 }; 125 | ASSERT_EQUAL_T(&exp0_0, &ds.pairs[0][0], type_point, NULL); 126 | ASSERT_EQUAL_T(&exp1_0, &ds.pairs[0][1], type_point, NULL); 127 | 128 | PASS(); 129 | } 130 | 131 | DEF_TEST(input_single_column_null) { 132 | init_pairs(&ds); 133 | char *lines[] = { 134 | "1278", 135 | "377", 136 | "316", 137 | "232", 138 | "_", 139 | "93", 140 | "63", 141 | "11", 142 | }; 143 | for (size_t i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) { 144 | char *line = lines[i]; 145 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, line, strlen(line), i)); 146 | } 147 | 148 | ASSERT_EQ(1, ds.columns); 149 | ASSERT_EQ(8, ds.rows); 150 | point exp0 = { .x = 0, .y = 1278 }; 151 | point exp1 = { .x = 1, .y = 377 }; 152 | point exp4 = { .x = 4, .y = EMPTY_VALUE }; 153 | point exp5 = { .x = 5, .y = 93 }; 154 | 155 | ASSERT_EQUAL_T(&exp0, &ds.pairs[0][0], type_point, NULL); 156 | ASSERT_EQUAL_T(&exp1, &ds.pairs[0][1], type_point, NULL); 157 | ASSERT_EQUAL_T(&exp4, &ds.pairs[0][4], type_point, NULL); 158 | ASSERT_EQUAL_T(&exp5, &ds.pairs[0][5], type_point, NULL); 159 | 160 | PASS(); 161 | } 162 | 163 | DEF_TEST(input_multiline_null) { 164 | init_pairs(&ds); 165 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "1,2,3", 5, 0)); 166 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "4,,6", 4, 1)); 167 | 168 | ASSERT_EQ(3, ds.columns); 169 | ASSERT_EQ(2, ds.rows); 170 | point exp0_0 = { .x = 0, .y = 1 }; 171 | point exp0_1 = { .x = 0, .y = 2 }; 172 | point exp0_2 = { .x = 0, .y = 3 }; 173 | 174 | point exp1_0 = { .x = 1, .y = 4 }; 175 | point exp1_1 = { .x = 1, .y = NAN }; 176 | point exp1_2 = { .x = 1, .y = 6 }; 177 | ASSERT_EQUAL_T(&exp0_0, &ds.pairs[0][0], type_point, NULL); 178 | ASSERT_EQUAL_T(&exp0_1, &ds.pairs[1][0], type_point, NULL); 179 | ASSERT_EQUAL_T(&exp0_2, &ds.pairs[2][0], type_point, NULL); 180 | ASSERT_EQUAL_T(&exp1_0, &ds.pairs[0][1], type_point, NULL); 181 | ASSERT_EQUAL_T(&exp1_1, &ds.pairs[1][1], type_point, NULL); 182 | ASSERT_EQUAL_T(&exp1_2, &ds.pairs[2][1], type_point, NULL); 183 | 184 | PASS(); 185 | } 186 | 187 | DEF_TEST(input_trailing_null) { 188 | init_pairs(&ds); 189 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "1,2,3,", 6, 0)); 190 | 191 | ASSERT_EQ(4, ds.columns); 192 | ASSERT_EQ(1, ds.rows); 193 | point exp0_0 = { .x = 0, .y = 1 }; 194 | point exp0_1 = { .x = 0, .y = 2 }; 195 | point exp0_2 = { .x = 0, .y = 3 }; 196 | point exp0_3 = { .x = 0, .y = NAN }; 197 | 198 | ASSERT_EQUAL_T(&exp0_0, &ds.pairs[0][0], type_point, NULL); 199 | ASSERT_EQUAL_T(&exp0_1, &ds.pairs[1][0], type_point, NULL); 200 | ASSERT_EQUAL_T(&exp0_2, &ds.pairs[2][0], type_point, NULL); 201 | ASSERT_EQUAL_T(&exp0_3, &ds.pairs[3][0], type_point, NULL); 202 | 203 | PASS(); 204 | } 205 | 206 | DEF_TEST(input_leading_null) { 207 | init_pairs(&ds); 208 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, ",1,2,3", 6, 0)); 209 | 210 | ASSERT_EQ_FMT(4, ds.columns, "%u"); 211 | ASSERT_EQ(1, ds.rows); 212 | point exp0_0 = { .x = 0, .y = NAN }; 213 | point exp0_1 = { .x = 0, .y = 1 }; 214 | point exp0_2 = { .x = 0, .y = 2 }; 215 | point exp0_3 = { .x = 0, .y = 3 }; 216 | 217 | ASSERT_EQUAL_T(&exp0_0, &ds.pairs[0][0], type_point, NULL); 218 | ASSERT_EQUAL_T(&exp0_1, &ds.pairs[1][0], type_point, NULL); 219 | ASSERT_EQUAL_T(&exp0_2, &ds.pairs[2][0], type_point, NULL); 220 | ASSERT_EQUAL_T(&exp0_3, &ds.pairs[3][0], type_point, NULL); 221 | 222 | PASS(); 223 | } 224 | 225 | DEF_TEST(row_with_more_columns_pads_previous_with_nulls) { 226 | init_pairs(&ds); 227 | // Pascal's Triangle 228 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "1", 1, 0)); 229 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "1,1", 3, 1)); 230 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, "1,2,1", 5, 2)); 231 | 232 | ASSERT_EQ(3, ds.columns); 233 | ASSERT_EQ(3, ds.rows); 234 | 235 | /* Expected data, exp[COL][ROW]. 236 | * The normal layout makes this a bit confusing. */ 237 | point exp[3][3] = { 238 | { { .x = 0, .y = 1}, 239 | { .x = 1, .y = 1}, 240 | { .x = 2, .y = 1}, }, 241 | { { .x = 0, .y = NAN}, 242 | { .x = 1, .y = 1}, 243 | { .x = 2, .y = 2}, }, 244 | { { .x = 0, .y = NAN}, 245 | { .x = 1, .y = NAN}, 246 | { .x = 2, .y = 1}, }, 247 | }; 248 | 249 | for (int r = 0; r < 3; r++) { 250 | for (int c = 0; c < 3; c++) { 251 | ASSERT_EQUAL_T(&exp[c][r], &ds.pairs[c][r], type_point, NULL); 252 | } 253 | } 254 | 255 | PASS(); 256 | } 257 | 258 | DEF_TEST(afl_crash0) { 259 | init_pairs(&ds); 260 | char *lines[] = { 261 | "-3 -3 -2", 262 | "- -2", 263 | "-\x01 -1", 264 | "0 1", 265 | "32", 266 | "-\x01 1", 267 | "3 \x11", 268 | "9 9", 269 | }; 270 | for (size_t i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) { 271 | char *line = lines[i]; 272 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, line, strlen(line), i)); 273 | } 274 | 275 | ASSERT_EQ(3, ds.columns); 276 | ASSERT_EQ(8, ds.rows); 277 | point exp[3][8] = { 278 | { { .x = 0, .y = -3}, 279 | { .x = 1, .y = NAN}, 280 | { .x = 2, .y = NAN}, 281 | { .x = 3, .y = 0}, 282 | { .x = 4, .y = 32}, 283 | { .x = 5, .y = NAN}, 284 | { .x = 6, .y = 3}, 285 | { .x = 7, .y = 9}, }, 286 | { { .x = 0, .y = -3}, 287 | { .x = 1, .y = NAN}, 288 | { .x = 2, .y = NAN}, 289 | { .x = 3, .y = 1}, 290 | { .x = 4, .y = NAN}, 291 | { .x = 5, .y = NAN}, 292 | { .x = 6, .y = NAN}, 293 | { .x = 7, .y = 9}, }, 294 | { { .x = 0, .y = -2}, 295 | { .x = 1, .y = NAN}, 296 | { .x = 2, .y = NAN}, 297 | { .x = 3, .y = NAN}, 298 | { .x = 4, .y = NAN}, 299 | { .x = 5, .y = NAN}, 300 | { .x = 6, .y = NAN}, 301 | { .x = 7, .y = NAN}, }, 302 | }; 303 | 304 | for (int r = 0; r < ds.rows; r++) { 305 | for (int c = 0; c < ds.columns; c++) { 306 | point *p = &ds.pairs[c][r]; 307 | if (GREATEST_IS_VERBOSE()) { printf("(%8g, %8g)\t", p->x, p->y); } 308 | ASSERT_EQUAL_T(&exp[c][r], p, type_point, NULL); 309 | } 310 | if (GREATEST_IS_VERBOSE()) { printf("\n"); } 311 | } 312 | 313 | PASS(); 314 | } 315 | 316 | DEF_TEST(afl_crash1) { 317 | init_pairs(&ds); 318 | char *lines[] = { 319 | "?", 320 | }; 321 | for (size_t i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) { 322 | char *line = lines[i]; 323 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, line, strlen(line), i)); 324 | } 325 | 326 | ASSERT_EQ(1, ds.columns); 327 | ASSERT_EQ(1, ds.rows); 328 | 329 | point exp = { .x = 0, .y = NAN }; 330 | ASSERT_EQUAL_T(&exp, &ds.pairs[0][0], type_point, NULL); 331 | PASS(); 332 | } 333 | 334 | DEF_TEST(afl_crash2) { 335 | init_pairs(&ds); 336 | char *lines[] = { 337 | "-3E3333333333333333", 338 | }; 339 | for (size_t i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) { 340 | char *line = lines[i]; 341 | ASSERT_EQ(SINK_LINE_OK, sink_line(&empty_cfg, &ds, line, strlen(line), i)); 342 | } 343 | 344 | ASSERT_EQ(1, ds.columns); 345 | ASSERT_EQ(1, ds.rows); 346 | 347 | point exp = { .x = 0, .y = EMPTY_VALUE }; 348 | ASSERT_EQUAL_T(&exp, &ds.pairs[0][0], type_point, NULL); 349 | PASS(); 350 | } 351 | 352 | SUITE(s_input) { 353 | SET_SETUP(setup_cb, NULL); 354 | SET_TEARDOWN(teardown_cb, NULL); 355 | 356 | RUN_TEST(input_empty); 357 | RUN_TEST(input_comment); 358 | RUN_TEST(input_single); 359 | RUN_TEST(input_pair); 360 | RUN_TEST(input_floats); 361 | RUN_TEST(input_csv); 362 | RUN_TEST(input_tab); 363 | RUN_TEST(input_exponent); 364 | RUN_TEST(input_multiline); 365 | 366 | // empty cell handling 367 | RUN_TEST(input_single_column_null); 368 | RUN_TEST(input_multiline_null); 369 | RUN_TEST(input_trailing_null); 370 | RUN_TEST(input_leading_null); 371 | RUN_TEST(row_with_more_columns_pads_previous_with_nulls); 372 | 373 | // fuzzer cases 374 | RUN_TEST(afl_crash0); 375 | RUN_TEST(afl_crash1); 376 | RUN_TEST(afl_crash2); 377 | } 378 | -------------------------------------------------------------------------------- /greatest.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2015 Scott Vokes 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #ifndef GREATEST_H 18 | #define GREATEST_H 19 | 20 | /* 1.0.0 */ 21 | #define GREATEST_VERSION_MAJOR 1 22 | #define GREATEST_VERSION_MINOR 0 23 | #define GREATEST_VERSION_PATCH 0 24 | 25 | /* A unit testing system for C, contained in 1 file. 26 | * It doesn't use dynamic allocation or depend on anything 27 | * beyond ANSI C89. */ 28 | 29 | 30 | /********************************************************************* 31 | * Minimal test runner template 32 | *********************************************************************/ 33 | #if 0 34 | 35 | #include "greatest.h" 36 | 37 | TEST foo_should_foo() { 38 | PASS(); 39 | } 40 | 41 | static void setup_cb(void *data) { 42 | printf("setup callback for each test case\n"); 43 | } 44 | 45 | static void teardown_cb(void *data) { 46 | printf("teardown callback for each test case\n"); 47 | } 48 | 49 | SUITE(suite) { 50 | /* Optional setup/teardown callbacks which will be run before/after 51 | * every test case in the suite. 52 | * Cleared when the suite finishes. */ 53 | SET_SETUP(setup_cb, voidp_to_callback_data); 54 | SET_TEARDOWN(teardown_cb, voidp_to_callback_data); 55 | 56 | RUN_TEST(foo_should_foo); 57 | } 58 | 59 | /* Add definitions that need to be in the test runner's main file. */ 60 | GREATEST_MAIN_DEFS(); 61 | 62 | /* Set up, run suite(s) of tests, report pass/fail/skip stats. */ 63 | int run_tests(void) { 64 | GREATEST_INIT(); /* init. greatest internals */ 65 | /* List of suites to run. */ 66 | RUN_SUITE(suite); 67 | GREATEST_REPORT(); /* display results */ 68 | return greatest_all_passed(); 69 | } 70 | 71 | /* main(), for a standalone command-line test runner. 72 | * This replaces run_tests above, and adds command line option 73 | * handling and exiting with a pass/fail status. */ 74 | int main(int argc, char **argv) { 75 | GREATEST_MAIN_BEGIN(); /* init & parse command-line args */ 76 | RUN_SUITE(suite); 77 | GREATEST_MAIN_END(); /* display results */ 78 | } 79 | 80 | #endif 81 | /*********************************************************************/ 82 | 83 | 84 | #include 85 | #include 86 | #include 87 | 88 | /*********** 89 | * Options * 90 | ***********/ 91 | 92 | /* Default column width for non-verbose output. */ 93 | #ifndef GREATEST_DEFAULT_WIDTH 94 | #define GREATEST_DEFAULT_WIDTH 72 95 | #endif 96 | 97 | /* FILE *, for test logging. */ 98 | #ifndef GREATEST_STDOUT 99 | #define GREATEST_STDOUT stdout 100 | #endif 101 | 102 | /* Remove GREATEST_ prefix from most commonly used symbols? */ 103 | #ifndef GREATEST_USE_ABBREVS 104 | #define GREATEST_USE_ABBREVS 1 105 | #endif 106 | 107 | /* Set to 0 to disable all use of setjmp/longjmp. */ 108 | #ifndef GREATEST_USE_LONGJMP 109 | #define GREATEST_USE_LONGJMP 1 110 | #endif 111 | 112 | #if GREATEST_USE_LONGJMP 113 | #include 114 | #endif 115 | 116 | /* Set to 0 to disable all use of time.h / clock(). */ 117 | #ifndef GREATEST_USE_TIME 118 | #define GREATEST_USE_TIME 1 119 | #endif 120 | 121 | #if GREATEST_USE_TIME 122 | #include 123 | #endif 124 | 125 | /* Floating point type, for ASSERT_IN_RANGE. */ 126 | #ifndef GREATEST_FLOAT 127 | #define GREATEST_FLOAT double 128 | #define GREATEST_FLOAT_FMT "%g" 129 | #endif 130 | 131 | /********* 132 | * Types * 133 | *********/ 134 | 135 | /* Info for the current running suite. */ 136 | typedef struct greatest_suite_info { 137 | unsigned int tests_run; 138 | unsigned int passed; 139 | unsigned int failed; 140 | unsigned int skipped; 141 | 142 | #if GREATEST_USE_TIME 143 | /* timers, pre/post running suite and individual tests */ 144 | clock_t pre_suite; 145 | clock_t post_suite; 146 | clock_t pre_test; 147 | clock_t post_test; 148 | #endif 149 | } greatest_suite_info; 150 | 151 | /* Type for a suite function. */ 152 | typedef void (greatest_suite_cb)(void); 153 | 154 | /* Types for setup/teardown callbacks. If non-NULL, these will be run 155 | * and passed the pointer to their additional data. */ 156 | typedef void (greatest_setup_cb)(void *udata); 157 | typedef void (greatest_teardown_cb)(void *udata); 158 | 159 | /* Type for an equality comparison between two pointers of the same type. 160 | * Should return non-0 if equal, otherwise 0. 161 | * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ 162 | typedef int greatest_equal_cb(const void *exp, const void *got, void *udata); 163 | 164 | /* Type for a callback that prints a value pointed to by T. 165 | * Return value has the same meaning as printf's. 166 | * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ 167 | typedef int greatest_printf_cb(const void *t, void *udata); 168 | 169 | /* Callbacks for an arbitrary type; needed for type-specific 170 | * comparisons via GREATEST_ASSERT_EQUAL_T[m].*/ 171 | typedef struct greatest_type_info { 172 | greatest_equal_cb *equal; 173 | greatest_printf_cb *print; 174 | } greatest_type_info; 175 | 176 | /* Callbacks for string type. */ 177 | extern greatest_type_info greatest_type_info_string; 178 | 179 | typedef enum { 180 | GREATEST_FLAG_VERBOSE = 0x01, 181 | GREATEST_FLAG_FIRST_FAIL = 0x02, 182 | GREATEST_FLAG_LIST_ONLY = 0x04 183 | } GREATEST_FLAG; 184 | 185 | /* Struct containing all test runner state. */ 186 | typedef struct greatest_run_info { 187 | unsigned int flags; 188 | unsigned int tests_run; /* total test count */ 189 | 190 | /* overall pass/fail/skip counts */ 191 | unsigned int passed; 192 | unsigned int failed; 193 | unsigned int skipped; 194 | unsigned int assertions; 195 | 196 | /* currently running test suite */ 197 | greatest_suite_info suite; 198 | 199 | /* info to print about the most recent failure */ 200 | const char *fail_file; 201 | unsigned int fail_line; 202 | const char *msg; 203 | 204 | /* current setup/teardown hooks and userdata */ 205 | greatest_setup_cb *setup; 206 | void *setup_udata; 207 | greatest_teardown_cb *teardown; 208 | void *teardown_udata; 209 | 210 | /* formatting info for ".....s...F"-style output */ 211 | unsigned int col; 212 | unsigned int width; 213 | 214 | /* only run a specific suite or test */ 215 | char *suite_filter; 216 | char *test_filter; 217 | 218 | #if GREATEST_USE_TIME 219 | /* overall timers */ 220 | clock_t begin; 221 | clock_t end; 222 | #endif 223 | 224 | #if GREATEST_USE_LONGJMP 225 | jmp_buf jump_dest; 226 | #endif 227 | } greatest_run_info; 228 | 229 | /* Global var for the current testing context. 230 | * Initialized by GREATEST_MAIN_DEFS(). */ 231 | extern greatest_run_info greatest_info; 232 | 233 | 234 | /********************** 235 | * Exported functions * 236 | **********************/ 237 | 238 | /* These are used internally by greatest. */ 239 | void greatest_do_pass(const char *name); 240 | void greatest_do_fail(const char *name); 241 | void greatest_do_skip(const char *name); 242 | int greatest_pre_test(const char *name); 243 | void greatest_post_test(const char *name, int res); 244 | void greatest_usage(const char *name); 245 | int greatest_do_assert_equal_t(const void *exp, const void *got, 246 | greatest_type_info *type_info, void *udata); 247 | 248 | /* These are part of the public greatest API. */ 249 | void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata); 250 | void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, void *udata); 251 | int greatest_all_passed(void); 252 | 253 | 254 | /******************** 255 | * Language Support * 256 | ********************/ 257 | 258 | /* If __VA_ARGS__ (C99) is supported, allow parametric testing 259 | * without needing to manually manage the argument struct. */ 260 | #if __STDC_VERSION__ >= 19901L || _MSC_VER >= 1800 261 | #define GREATEST_VA_ARGS 262 | #endif 263 | 264 | 265 | /********** 266 | * Macros * 267 | **********/ 268 | 269 | /* Define a suite. */ 270 | #define GREATEST_SUITE(NAME) void NAME(void); void NAME(void) 271 | 272 | /* Start defining a test function. 273 | * The arguments are not included, to allow parametric testing. */ 274 | #define GREATEST_TEST static greatest_test_res 275 | 276 | /* PASS/FAIL/SKIP result from a test. Used internally. */ 277 | typedef enum { 278 | GREATEST_TEST_RES_PASS = 0, 279 | GREATEST_TEST_RES_FAIL = -1, 280 | GREATEST_TEST_RES_SKIP = 1 281 | } greatest_test_res; 282 | 283 | /* Run a suite. */ 284 | #define GREATEST_RUN_SUITE(S_NAME) greatest_run_suite(S_NAME, #S_NAME) 285 | 286 | /* Run a test in the current suite. */ 287 | #define GREATEST_RUN_TEST(TEST) \ 288 | do { \ 289 | if (greatest_pre_test(#TEST) == 1) { \ 290 | greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ 291 | if (res == GREATEST_TEST_RES_PASS) { \ 292 | res = TEST(); \ 293 | } \ 294 | greatest_post_test(#TEST, res); \ 295 | } else if (GREATEST_LIST_ONLY()) { \ 296 | fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ 297 | } \ 298 | } while (0) 299 | 300 | /* Run a test in the current suite with one void * argument, 301 | * which can be a pointer to a struct with multiple arguments. */ 302 | #define GREATEST_RUN_TEST1(TEST, ENV) \ 303 | do { \ 304 | if (greatest_pre_test(#TEST) == 1) { \ 305 | int res = TEST(ENV); \ 306 | greatest_post_test(#TEST, res); \ 307 | } else if (GREATEST_LIST_ONLY()) { \ 308 | fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ 309 | } \ 310 | } while (0) 311 | 312 | #ifdef GREATEST_VA_ARGS 313 | #define GREATEST_RUN_TESTp(TEST, ...) \ 314 | do { \ 315 | if (greatest_pre_test(#TEST) == 1) { \ 316 | int res = TEST(__VA_ARGS__); \ 317 | greatest_post_test(#TEST, res); \ 318 | } else if (GREATEST_LIST_ONLY()) { \ 319 | fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ 320 | } \ 321 | } while (0) 322 | #endif 323 | 324 | 325 | /* Check if the test runner is in verbose mode. */ 326 | #define GREATEST_IS_VERBOSE() (greatest_info.flags & GREATEST_FLAG_VERBOSE) 327 | #define GREATEST_LIST_ONLY() (greatest_info.flags & GREATEST_FLAG_LIST_ONLY) 328 | #define GREATEST_FIRST_FAIL() (greatest_info.flags & GREATEST_FLAG_FIRST_FAIL) 329 | #define GREATEST_FAILURE_ABORT() (greatest_info.suite.failed > 0 && GREATEST_FIRST_FAIL()) 330 | 331 | /* Message-less forms of tests defined below. */ 332 | #define GREATEST_PASS() GREATEST_PASSm(NULL) 333 | #define GREATEST_FAIL() GREATEST_FAILm(NULL) 334 | #define GREATEST_SKIP() GREATEST_SKIPm(NULL) 335 | #define GREATEST_ASSERT(COND) \ 336 | GREATEST_ASSERTm(#COND, COND) 337 | #define GREATEST_ASSERT_OR_LONGJMP(COND) \ 338 | GREATEST_ASSERT_OR_LONGJMPm(#COND, COND) 339 | #define GREATEST_ASSERT_FALSE(COND) \ 340 | GREATEST_ASSERT_FALSEm(#COND, COND) 341 | #define GREATEST_ASSERT_EQ(EXP, GOT) \ 342 | GREATEST_ASSERT_EQm(#EXP " != " #GOT, EXP, GOT) 343 | #define GREATEST_ASSERT_EQ_FMT(EXP, GOT, FMT) \ 344 | GREATEST_ASSERT_EQ_FMTm(#EXP " != " #GOT, EXP, GOT, FMT) 345 | #define GREATEST_ASSERT_IN_RANGE(EXP, GOT, TOL) \ 346 | GREATEST_ASSERT_IN_RANGEm(#EXP " != " #GOT " +/- " #TOL, EXP, GOT, TOL) 347 | #define GREATEST_ASSERT_EQUAL_T(EXP, GOT, TYPE_INFO, UDATA) \ 348 | GREATEST_ASSERT_EQUAL_Tm(#EXP " != " #GOT, EXP, GOT, TYPE_INFO, UDATA) 349 | #define GREATEST_ASSERT_STR_EQ(EXP, GOT) \ 350 | GREATEST_ASSERT_STR_EQm(#EXP " != " #GOT, EXP, GOT) 351 | 352 | /* The following forms take an additional message argument first, 353 | * to be displayed by the test runner. */ 354 | 355 | /* Fail if a condition is not true, with message. */ 356 | #define GREATEST_ASSERTm(MSG, COND) \ 357 | do { \ 358 | greatest_info.assertions++; \ 359 | if (!(COND)) { GREATEST_FAILm(MSG); } \ 360 | } while (0) 361 | 362 | /* Fail if a condition is not true, longjmping out of test. */ 363 | #define GREATEST_ASSERT_OR_LONGJMPm(MSG, COND) \ 364 | do { \ 365 | greatest_info.assertions++; \ 366 | if (!(COND)) { GREATEST_FAIL_WITH_LONGJMPm(MSG); } \ 367 | } while (0) 368 | 369 | /* Fail if a condition is not false, with message. */ 370 | #define GREATEST_ASSERT_FALSEm(MSG, COND) \ 371 | do { \ 372 | greatest_info.assertions++; \ 373 | if ((COND)) { GREATEST_FAILm(MSG); } \ 374 | } while (0) 375 | 376 | /* Fail if EXP != GOT (equality comparison by ==). */ 377 | #define GREATEST_ASSERT_EQm(MSG, EXP, GOT) \ 378 | do { \ 379 | greatest_info.assertions++; \ 380 | if ((EXP) != (GOT)) { GREATEST_FAILm(MSG); } \ 381 | } while (0) 382 | 383 | /* Fail if EXP != GOT (equality comparison by ==). */ 384 | #define GREATEST_ASSERT_EQ_FMTm(MSG, EXP, GOT, FMT) \ 385 | do { \ 386 | greatest_info.assertions++; \ 387 | const char *fmt = ( FMT ); \ 388 | if ((EXP) != (GOT)) { \ 389 | fprintf(GREATEST_STDOUT, "\nExpected: "); \ 390 | fprintf(GREATEST_STDOUT, fmt, EXP); \ 391 | fprintf(GREATEST_STDOUT, "\nGot: "); \ 392 | fprintf(GREATEST_STDOUT, fmt, GOT); \ 393 | fprintf(GREATEST_STDOUT, "\n"); \ 394 | GREATEST_FAILm(MSG); \ 395 | } \ 396 | } while (0) 397 | 398 | /* Fail if GOT not in range of EXP +|- TOL. */ 399 | #define GREATEST_ASSERT_IN_RANGEm(MSG, EXP, GOT, TOL) \ 400 | do { \ 401 | greatest_info.assertions++; \ 402 | GREATEST_FLOAT exp = (EXP); \ 403 | GREATEST_FLOAT got = (GOT); \ 404 | GREATEST_FLOAT tol = (TOL); \ 405 | if ((exp > got && exp - got > tol) || \ 406 | (exp < got && got - exp > tol)) { \ 407 | fprintf(GREATEST_STDOUT, \ 408 | "\nExpected: " GREATEST_FLOAT_FMT \ 409 | " +/- " GREATEST_FLOAT_FMT "\n" \ 410 | "Got: " GREATEST_FLOAT_FMT "\n", \ 411 | exp, tol, got); \ 412 | GREATEST_FAILm(MSG); \ 413 | } \ 414 | } while (0) 415 | 416 | /* Fail if EXP is not equal to GOT, according to strcmp. */ 417 | #define GREATEST_ASSERT_STR_EQm(MSG, EXP, GOT) \ 418 | do { \ 419 | GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ 420 | &greatest_type_info_string, NULL); \ 421 | } while (0) \ 422 | 423 | /* Fail if EXP is not equal to GOT, according to a comparison 424 | * callback in TYPE_INFO. If they are not equal, optionally use a 425 | * print callback in TYPE_INFO to print them. */ 426 | #define GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, TYPE_INFO, UDATA) \ 427 | do { \ 428 | greatest_type_info *type_info = (TYPE_INFO); \ 429 | greatest_info.assertions++; \ 430 | if (!greatest_do_assert_equal_t(EXP, GOT, \ 431 | type_info, UDATA)) { \ 432 | if (type_info == NULL || type_info->equal == NULL) { \ 433 | GREATEST_FAILm("type_info->equal callback missing!"); \ 434 | } else { \ 435 | GREATEST_FAILm(MSG); \ 436 | } \ 437 | } \ 438 | } while (0) \ 439 | 440 | /* Pass. */ 441 | #define GREATEST_PASSm(MSG) \ 442 | do { \ 443 | greatest_info.msg = MSG; \ 444 | return GREATEST_TEST_RES_PASS; \ 445 | } while (0) 446 | 447 | /* Fail. */ 448 | #define GREATEST_FAILm(MSG) \ 449 | do { \ 450 | greatest_info.fail_file = __FILE__; \ 451 | greatest_info.fail_line = __LINE__; \ 452 | greatest_info.msg = MSG; \ 453 | return GREATEST_TEST_RES_FAIL; \ 454 | } while (0) 455 | 456 | /* Optional GREATEST_FAILm variant that longjmps. */ 457 | #if GREATEST_USE_LONGJMP 458 | #define GREATEST_FAIL_WITH_LONGJMP() GREATEST_FAIL_WITH_LONGJMPm(NULL) 459 | #define GREATEST_FAIL_WITH_LONGJMPm(MSG) \ 460 | do { \ 461 | greatest_info.fail_file = __FILE__; \ 462 | greatest_info.fail_line = __LINE__; \ 463 | greatest_info.msg = MSG; \ 464 | longjmp(greatest_info.jump_dest, GREATEST_TEST_RES_FAIL); \ 465 | } while (0) 466 | #endif 467 | 468 | /* Skip the current test. */ 469 | #define GREATEST_SKIPm(MSG) \ 470 | do { \ 471 | greatest_info.msg = MSG; \ 472 | return GREATEST_TEST_RES_SKIP; \ 473 | } while (0) 474 | 475 | /* Check the result of a subfunction using ASSERT, etc. */ 476 | #define GREATEST_CHECK_CALL(RES) \ 477 | do { \ 478 | int _check_call_res = RES; \ 479 | if (_check_call_res != GREATEST_TEST_RES_PASS) { \ 480 | return _check_call_res; \ 481 | } \ 482 | } while (0) \ 483 | 484 | #if GREATEST_USE_TIME 485 | #define GREATEST_SET_TIME(NAME) \ 486 | NAME = clock(); \ 487 | if (NAME == (clock_t) -1) { \ 488 | fprintf(GREATEST_STDOUT, \ 489 | "clock error: %s\n", #NAME); \ 490 | exit(EXIT_FAILURE); \ 491 | } 492 | 493 | #define GREATEST_CLOCK_DIFF(C1, C2) \ 494 | fprintf(GREATEST_STDOUT, " (%lu ticks, %.3f sec)", \ 495 | (long unsigned int) (C2) - (long unsigned int)(C1), \ 496 | (double)((C2) - (C1)) / (1.0 * (double)CLOCKS_PER_SEC)) 497 | #else 498 | #define GREATEST_SET_TIME(UNUSED) 499 | #define GREATEST_CLOCK_DIFF(UNUSED1, UNUSED2) 500 | #endif 501 | 502 | #if GREATEST_USE_LONGJMP 503 | #define GREATEST_SAVE_CONTEXT() \ 504 | /* setjmp returns 0 (GREATEST_TEST_RES_PASS) on first call */ \ 505 | /* so the test runs, then RES_FAIL from FAIL_WITH_LONGJMP. */ \ 506 | ((greatest_test_res)(setjmp(greatest_info.jump_dest))) 507 | #else 508 | #define GREATEST_SAVE_CONTEXT() \ 509 | /*a no-op, since setjmp/longjmp aren't being used */ \ 510 | GREATEST_TEST_RES_PASS 511 | #endif 512 | 513 | /* Include several function definitions in the main test file. */ 514 | #define GREATEST_MAIN_DEFS() \ 515 | \ 516 | /* Is FILTER a subset of NAME? */ \ 517 | static int greatest_name_match(const char *name, \ 518 | const char *filter) { \ 519 | size_t offset = 0; \ 520 | size_t filter_len = strlen(filter); \ 521 | while (name[offset] != '\0') { \ 522 | if (name[offset] == filter[0]) { \ 523 | if (0 == strncmp(&name[offset], filter, filter_len)) { \ 524 | return 1; \ 525 | } \ 526 | } \ 527 | offset++; \ 528 | } \ 529 | \ 530 | return 0; \ 531 | } \ 532 | \ 533 | int greatest_pre_test(const char *name) { \ 534 | if (!GREATEST_LIST_ONLY() \ 535 | && (!GREATEST_FIRST_FAIL() || greatest_info.suite.failed == 0) \ 536 | && (greatest_info.test_filter == NULL || \ 537 | greatest_name_match(name, greatest_info.test_filter))) { \ 538 | GREATEST_SET_TIME(greatest_info.suite.pre_test); \ 539 | if (greatest_info.setup) { \ 540 | greatest_info.setup(greatest_info.setup_udata); \ 541 | } \ 542 | return 1; /* test should be run */ \ 543 | } else { \ 544 | return 0; /* skipped */ \ 545 | } \ 546 | } \ 547 | \ 548 | void greatest_post_test(const char *name, int res) { \ 549 | GREATEST_SET_TIME(greatest_info.suite.post_test); \ 550 | if (greatest_info.teardown) { \ 551 | void *udata = greatest_info.teardown_udata; \ 552 | greatest_info.teardown(udata); \ 553 | } \ 554 | \ 555 | if (res <= GREATEST_TEST_RES_FAIL) { \ 556 | greatest_do_fail(name); \ 557 | } else if (res >= GREATEST_TEST_RES_SKIP) { \ 558 | greatest_do_skip(name); \ 559 | } else if (res == GREATEST_TEST_RES_PASS) { \ 560 | greatest_do_pass(name); \ 561 | } \ 562 | greatest_info.suite.tests_run++; \ 563 | greatest_info.col++; \ 564 | if (GREATEST_IS_VERBOSE()) { \ 565 | GREATEST_CLOCK_DIFF(greatest_info.suite.pre_test, \ 566 | greatest_info.suite.post_test); \ 567 | fprintf(GREATEST_STDOUT, "\n"); \ 568 | } else if (greatest_info.col % greatest_info.width == 0) { \ 569 | fprintf(GREATEST_STDOUT, "\n"); \ 570 | greatest_info.col = 0; \ 571 | } \ 572 | if (GREATEST_STDOUT == stdout) fflush(stdout); \ 573 | } \ 574 | \ 575 | static void greatest_run_suite(greatest_suite_cb *suite_cb, \ 576 | const char *suite_name) { \ 577 | if (greatest_info.suite_filter && \ 578 | !greatest_name_match(suite_name, greatest_info.suite_filter)) { \ 579 | return; \ 580 | } \ 581 | if (GREATEST_FIRST_FAIL() && greatest_info.failed > 0) { return; } \ 582 | memset(&greatest_info.suite, 0, sizeof(greatest_info.suite)); \ 583 | greatest_info.col = 0; \ 584 | fprintf(GREATEST_STDOUT, "\n* Suite %s:\n", suite_name); \ 585 | GREATEST_SET_TIME(greatest_info.suite.pre_suite); \ 586 | suite_cb(); \ 587 | GREATEST_SET_TIME(greatest_info.suite.post_suite); \ 588 | if (greatest_info.suite.tests_run > 0) { \ 589 | fprintf(GREATEST_STDOUT, \ 590 | "\n%u tests - %u pass, %u fail, %u skipped", \ 591 | greatest_info.suite.tests_run, \ 592 | greatest_info.suite.passed, \ 593 | greatest_info.suite.failed, \ 594 | greatest_info.suite.skipped); \ 595 | GREATEST_CLOCK_DIFF(greatest_info.suite.pre_suite, \ 596 | greatest_info.suite.post_suite); \ 597 | fprintf(GREATEST_STDOUT, "\n"); \ 598 | } \ 599 | greatest_info.setup = NULL; \ 600 | greatest_info.setup_udata = NULL; \ 601 | greatest_info.teardown = NULL; \ 602 | greatest_info.teardown_udata = NULL; \ 603 | greatest_info.passed += greatest_info.suite.passed; \ 604 | greatest_info.failed += greatest_info.suite.failed; \ 605 | greatest_info.skipped += greatest_info.suite.skipped; \ 606 | greatest_info.tests_run += greatest_info.suite.tests_run; \ 607 | } \ 608 | \ 609 | void greatest_do_pass(const char *name) { \ 610 | if (GREATEST_IS_VERBOSE()) { \ 611 | fprintf(GREATEST_STDOUT, "PASS %s: %s", \ 612 | name, greatest_info.msg ? greatest_info.msg : ""); \ 613 | } else { \ 614 | fprintf(GREATEST_STDOUT, "."); \ 615 | } \ 616 | greatest_info.suite.passed++; \ 617 | } \ 618 | \ 619 | void greatest_do_fail(const char *name) { \ 620 | if (GREATEST_IS_VERBOSE()) { \ 621 | fprintf(GREATEST_STDOUT, \ 622 | "FAIL %s: %s (%s:%u)", \ 623 | name, greatest_info.msg ? greatest_info.msg : "", \ 624 | greatest_info.fail_file, greatest_info.fail_line); \ 625 | } else { \ 626 | fprintf(GREATEST_STDOUT, "F"); \ 627 | greatest_info.col++; \ 628 | /* add linebreak if in line of '.'s */ \ 629 | if (greatest_info.col != 0) { \ 630 | fprintf(GREATEST_STDOUT, "\n"); \ 631 | greatest_info.col = 0; \ 632 | } \ 633 | fprintf(GREATEST_STDOUT, "FAIL %s: %s (%s:%u)\n", \ 634 | name, \ 635 | greatest_info.msg ? greatest_info.msg : "", \ 636 | greatest_info.fail_file, greatest_info.fail_line); \ 637 | } \ 638 | greatest_info.suite.failed++; \ 639 | } \ 640 | \ 641 | void greatest_do_skip(const char *name) { \ 642 | if (GREATEST_IS_VERBOSE()) { \ 643 | fprintf(GREATEST_STDOUT, "SKIP %s: %s", \ 644 | name, \ 645 | greatest_info.msg ? \ 646 | greatest_info.msg : "" ); \ 647 | } else { \ 648 | fprintf(GREATEST_STDOUT, "s"); \ 649 | } \ 650 | greatest_info.suite.skipped++; \ 651 | } \ 652 | \ 653 | int greatest_do_assert_equal_t(const void *exp, const void *got, \ 654 | greatest_type_info *type_info, void *udata) { \ 655 | int eq = 0; \ 656 | if (type_info == NULL || type_info->equal == NULL) { \ 657 | return 0; \ 658 | } \ 659 | eq = type_info->equal(exp, got, udata); \ 660 | if (!eq) { \ 661 | if (type_info->print != NULL) { \ 662 | fprintf(GREATEST_STDOUT, "\nExpected: "); \ 663 | (void)type_info->print(exp, udata); \ 664 | fprintf(GREATEST_STDOUT, "\nGot: "); \ 665 | (void)type_info->print(got, udata); \ 666 | fprintf(GREATEST_STDOUT, "\n"); \ 667 | } else { \ 668 | fprintf(GREATEST_STDOUT, \ 669 | "GREATEST_ASSERT_EQUAL_T failure at %s:%dn", \ 670 | greatest_info.fail_file, \ 671 | greatest_info.fail_line); \ 672 | } \ 673 | } \ 674 | return eq; \ 675 | } \ 676 | \ 677 | void greatest_usage(const char *name) { \ 678 | fprintf(GREATEST_STDOUT, \ 679 | "Usage: %s [-hlfv] [-s SUITE] [-t TEST]\n" \ 680 | " -h print this Help\n" \ 681 | " -l List suites and their tests, then exit\n" \ 682 | " -f Stop runner after first failure\n" \ 683 | " -v Verbose output\n" \ 684 | " -s SUITE only run suite named SUITE\n" \ 685 | " -t TEST only run test named TEST\n", \ 686 | name); \ 687 | } \ 688 | \ 689 | int greatest_all_passed() { return (greatest_info.failed == 0); } \ 690 | \ 691 | void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata) { \ 692 | greatest_info.setup = cb; \ 693 | greatest_info.setup_udata = udata; \ 694 | } \ 695 | \ 696 | void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, \ 697 | void *udata) { \ 698 | greatest_info.teardown = cb; \ 699 | greatest_info.teardown_udata = udata; \ 700 | } \ 701 | \ 702 | static int greatest_string_equal_cb(const void *exp, const void *got, \ 703 | void *udata) { \ 704 | (void)udata; \ 705 | return (0 == strcmp((const char *)exp, (const char *)got)); \ 706 | } \ 707 | \ 708 | static int greatest_string_printf_cb(const void *t, void *udata) { \ 709 | (void)udata; \ 710 | return fprintf(GREATEST_STDOUT, "%s", (const char *)t); \ 711 | } \ 712 | \ 713 | greatest_type_info greatest_type_info_string = { \ 714 | greatest_string_equal_cb, \ 715 | greatest_string_printf_cb, \ 716 | }; \ 717 | \ 718 | greatest_run_info greatest_info 719 | 720 | /* Init internals. */ 721 | #define GREATEST_INIT() \ 722 | do { \ 723 | memset(&greatest_info, 0, sizeof(greatest_info)); \ 724 | greatest_info.width = GREATEST_DEFAULT_WIDTH; \ 725 | GREATEST_SET_TIME(greatest_info.begin); \ 726 | } while (0) \ 727 | 728 | /* Handle command-line arguments, etc. */ 729 | #define GREATEST_MAIN_BEGIN() \ 730 | do { \ 731 | int i = 0; \ 732 | GREATEST_INIT(); \ 733 | for (i = 1; i < argc; i++) { \ 734 | if (0 == strcmp("-t", argv[i])) { \ 735 | if (argc <= i + 1) { \ 736 | greatest_usage(argv[0]); \ 737 | exit(EXIT_FAILURE); \ 738 | } \ 739 | greatest_info.test_filter = argv[i+1]; \ 740 | i++; \ 741 | } else if (0 == strcmp("-s", argv[i])) { \ 742 | if (argc <= i + 1) { \ 743 | greatest_usage(argv[0]); \ 744 | exit(EXIT_FAILURE); \ 745 | } \ 746 | greatest_info.suite_filter = argv[i+1]; \ 747 | i++; \ 748 | } else if (0 == strcmp("-f", argv[i])) { \ 749 | greatest_info.flags |= GREATEST_FLAG_FIRST_FAIL; \ 750 | } else if (0 == strcmp("-v", argv[i])) { \ 751 | greatest_info.flags |= GREATEST_FLAG_VERBOSE; \ 752 | } else if (0 == strcmp("-l", argv[i])) { \ 753 | greatest_info.flags |= GREATEST_FLAG_LIST_ONLY; \ 754 | } else if (0 == strcmp("-h", argv[i])) { \ 755 | greatest_usage(argv[0]); \ 756 | exit(EXIT_SUCCESS); \ 757 | } else { \ 758 | fprintf(GREATEST_STDOUT, \ 759 | "Unknown argument '%s'\n", argv[i]); \ 760 | greatest_usage(argv[0]); \ 761 | exit(EXIT_FAILURE); \ 762 | } \ 763 | } \ 764 | } while (0) 765 | 766 | /* Report passes, failures, skipped tests, the number of 767 | * assertions, and the overall run time. */ 768 | #define GREATEST_REPORT() \ 769 | do { \ 770 | if (!GREATEST_LIST_ONLY()) { \ 771 | GREATEST_SET_TIME(greatest_info.end); \ 772 | fprintf(GREATEST_STDOUT, \ 773 | "\nTotal: %u tests", greatest_info.tests_run); \ 774 | GREATEST_CLOCK_DIFF(greatest_info.begin, \ 775 | greatest_info.end); \ 776 | fprintf(GREATEST_STDOUT, ", %u assertions\n", \ 777 | greatest_info.assertions); \ 778 | fprintf(GREATEST_STDOUT, \ 779 | "Pass: %u, fail: %u, skip: %u.\n", \ 780 | greatest_info.passed, \ 781 | greatest_info.failed, greatest_info.skipped); \ 782 | } \ 783 | } while (0) 784 | 785 | /* Report results, exit with exit status based on results. */ 786 | #define GREATEST_MAIN_END() \ 787 | do { \ 788 | GREATEST_REPORT(); \ 789 | return (greatest_all_passed() ? EXIT_SUCCESS : EXIT_FAILURE); \ 790 | } while (0) 791 | 792 | /* Make abbreviations without the GREATEST_ prefix for the 793 | * most commonly used symbols. */ 794 | #if GREATEST_USE_ABBREVS 795 | #define TEST GREATEST_TEST 796 | #define SUITE GREATEST_SUITE 797 | #define RUN_TEST GREATEST_RUN_TEST 798 | #define RUN_TEST1 GREATEST_RUN_TEST1 799 | #define RUN_SUITE GREATEST_RUN_SUITE 800 | #define ASSERT GREATEST_ASSERT 801 | #define ASSERTm GREATEST_ASSERTm 802 | #define ASSERT_FALSE GREATEST_ASSERT_FALSE 803 | #define ASSERT_EQ GREATEST_ASSERT_EQ 804 | #define ASSERT_EQ_FMT GREATEST_ASSERT_EQ_FMT 805 | #define ASSERT_IN_RANGE GREATEST_ASSERT_IN_RANGE 806 | #define ASSERT_EQUAL_T GREATEST_ASSERT_EQUAL_T 807 | #define ASSERT_STR_EQ GREATEST_ASSERT_STR_EQ 808 | #define ASSERT_FALSEm GREATEST_ASSERT_FALSEm 809 | #define ASSERT_EQm GREATEST_ASSERT_EQm 810 | #define ASSERT_EQ_FMTm GREATEST_ASSERT_EQ_FMTm 811 | #define ASSERT_IN_RANGEm GREATEST_ASSERT_IN_RANGEm 812 | #define ASSERT_EQUAL_Tm GREATEST_ASSERT_EQUAL_Tm 813 | #define ASSERT_STR_EQm GREATEST_ASSERT_STR_EQm 814 | #define PASS GREATEST_PASS 815 | #define FAIL GREATEST_FAIL 816 | #define SKIP GREATEST_SKIP 817 | #define PASSm GREATEST_PASSm 818 | #define FAILm GREATEST_FAILm 819 | #define SKIPm GREATEST_SKIPm 820 | #define SET_SETUP GREATEST_SET_SETUP_CB 821 | #define SET_TEARDOWN GREATEST_SET_TEARDOWN_CB 822 | #define CHECK_CALL GREATEST_CHECK_CALL 823 | 824 | #ifdef GREATEST_VA_ARGS 825 | #define RUN_TESTp GREATEST_RUN_TESTp 826 | #endif 827 | 828 | #if GREATEST_USE_LONGJMP 829 | #define ASSERT_OR_LONGJMP GREATEST_ASSERT_OR_LONGJMP 830 | #define ASSERT_OR_LONGJMPm GREATEST_ASSERT_OR_LONGJMPm 831 | #define FAIL_WITH_LONGJMP GREATEST_FAIL_WITH_LONGJMP 832 | #define FAIL_WITH_LONGJMPm GREATEST_FAIL_WITH_LONGJMPm 833 | #endif 834 | 835 | #endif /* USE_ABBREVS */ 836 | 837 | #endif 838 | --------------------------------------------------------------------------------