commit fdf9f6179562d206a12a2b3953b4c87ec4c25641 Author: Paul J. Davis Date: Sun Apr 3 19:27:04 2011 -0400 Initial import. Passes all eep0018 tests. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32533cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.o +*.so +ebin/jiffy.app diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d573bb6 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + +all: build + +%.beam: %.erl + erlc -o test/ $< + +build: c_src/decoder.c + ./rebar compile + +check: test/etap.beam test/util.beam + prove test/*.t diff --git a/c_src/decoder.c b/c_src/decoder.c new file mode 100644 index 0000000..16effd6 --- /dev/null +++ b/c_src/decoder.c @@ -0,0 +1,934 @@ +#include +#include +#include +#include +#include + +#include "erl_nif.h" +#include "jiffy.h" + +#define U(c) ((unsigned char) (c)) +#define ERROR(i, msg) make_error(st, env, msg) + +#define STACK_SIZE_INC 64 +#define NUM_BUF_LEN 256 + +enum { + st_value=0, + st_object, + st_array, + st_key, + st_colon, + st_comma, + st_done, + st_invalid +} JsonState; + +enum { + nst_init=0, + nst_sign, + nst_mantissa, + nst_frac0, + nst_frac1, + nst_frac, + nst_esign, + nst_edigit +} JsonNumState; + +typedef struct { + ErlNifEnv* env; + jiffy_st* atoms; + + ERL_NIF_TERM arg; + ErlNifBinary bin; + + int has_bignum; + + char* p; + unsigned char* u; + int i; + int len; + + char* st_data; + int st_size; + int st_top; +} Decoder; + +void +dec_init(Decoder* d, ErlNifEnv* env, ERL_NIF_TERM arg, ErlNifBinary* bin) +{ + int i; + + d->env = env; + d->atoms = enif_priv_data(env); + d->arg = arg; + + d->has_bignum = 0; + + d->p = (char*) bin->data; + d->u = bin->data; + d->len = bin->size; + d->i = 0; + + d->st_data = (char*) enif_alloc(STACK_SIZE_INC * sizeof(char)); + d->st_size = STACK_SIZE_INC; + d->st_top = 0; + + for(i = 0; i < d->st_size; i++) { + d->st_data[i] = st_invalid; + } + + d->st_data[0] = st_value; + d->st_top++; +} + +void +dec_destroy(Decoder* d) +{ + if(d->st_data != NULL) { + enif_free(d->st_data); + } +} + +ERL_NIF_TERM +dec_error(Decoder* d, const char* atom) +{ + ERL_NIF_TERM pos = enif_make_int(d->env, d->i+1); + ERL_NIF_TERM msg = make_atom(d->env, atom); + ERL_NIF_TERM ret = enif_make_tuple2(d->env, pos, msg); + return enif_make_tuple2(d->env, d->atoms->atom_error, ret); +} + +char +dec_curr(Decoder* d) +{ + return d->st_data[d->st_top-1]; +} + +int +dec_top(Decoder* d) +{ + return d->st_top; +} + +void +dec_push(Decoder* d, char val) +{ + char* tmp; + int new_sz; + int i; + + if(d->st_top >= d->st_size) { + new_sz = d->st_size + STACK_SIZE_INC; + tmp = (char*) enif_alloc(new_sz * sizeof(char)); + memcpy(tmp, d->st_data, d->st_size * sizeof(char)); + enif_free(d->st_data); + d->st_data = tmp; + d->st_size = new_sz; + for(i = d->st_top; i < d->st_size; i++) { + d->st_data[i] = st_invalid; + } + } + + d->st_data[d->st_top++] = val; +} + +void +dec_pop(Decoder* d, char val) +{ + assert(d->st_data[d->st_top-1] == val && "popped invalid state."); + d->st_data[d->st_top-1] = st_invalid; + d->st_top--; +} + +int +dec_string(Decoder* d, ERL_NIF_TERM* value) +{ + int has_escape = 0; + int num_escapes = 0; + int st; + int ulen; + int ui; + int hi; + int lo; + char* chrbuf; + int chrpos; + + if(d->p[d->i] != '\"') { + return 0; + } + d->i++; + + st = d->i; + + while(d->i < d->len) { + if(d->u[d->i] < 0x20) { + return 0; + } else if(d->p[d->i] == '\"') { + d->i++; + goto parse; + } else if(d->p[d->i] == '\\') { + if(d->i+1 >= d->len) { + return 0; + } + has_escape = 1; + num_escapes += 1; + d->i++; + switch(d->p[d->i]) { + case '\"': + case '\\': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + d->i++; + break; + case 'u': + hi = 0; + lo = 0; + d->i++; + if(d->i + 4 >= d->len) { + return 0; + } + hi = int_from_hex(&(d->u[d->i])); + d->i += 4; + if(hi < 0) { + return 0; + } + if(hi >= 0xD800 && hi < 0xDC00) { + if(d->i + 6 >= d->len) { + return 0; + } + if(d->p[d->i++] != '\\') { + return 0; + } else if(d->p[d->i++] != 'u') { + return 0; + } + lo = int_from_hex(&(d->u[d->i])); + if(lo < 0) { + return 0; + } + hi = utf8_from_pair(hi, lo); + if(hi < 0) { + return 0; + } + } + hi = utf8_len(hi); + if(hi < 0) { + return 0; + } + if(lo == 0) { + num_escapes += 5 - hi; + } else { + num_escapes += 11 - hi; + } + break; + default: + return 0; + } + } else if(d->u[d->i] < 0x80) { + d->i++; + } else { + ulen = -1; + if((d->u[d->i] & 0xE0) == 0xC0) { + ulen = 1; + } else if((d->u[d->i] & 0xF0) == 0xE0) { + ulen = 2; + } else if((d->u[d->i] & 0xF8) == 0xF0) { + ulen = 3; + } else if((d->u[d->i] & 0xFC) == 0xF8) { + ulen = 4; + } else if((d->u[d->i] & 0xFE) == 0xFC) { + ulen = 5; + } + if(ulen < 0) { + return 0; + } + if(d->i + ulen >= d->len) { + return 0; + } + for(ui = 0; ui < ulen; ui++) { + if((d->u[d->i+1+ui] & 0xC0) != 0x80) { + return 0; + } + } + // Wikipedia says I have to check that a UTF-8 encoding + // uses as few bits as possible. This means that we + // can't do things like encode 't' in three bytes. + // To check this all we need to ensure is that for each + // of the following bit patterns that there is at least + // one 1 bit in each of the x's + // 11: 110xxxxy 10yyyyyy + // 16: 1110xxxx 10xyyyyy 10yyyyyy + // 21: 11110xxx 10xxyyyy 10yyyyyy 10yyyyyy + // 26: 111110xx 10xxxyyy 10yyyyyy 10yyyyyy 10yyyyyy + // 31: 1111110x 10xxxxyy 10yyyyyy 10yyyyyy 10yyyyyy 10yyyyyy + if(ulen == 1) { + if((d->u[d->i] & 0x1E) == 0) return 0; + } else if(ulen == 2) { + if((d->u[d->i] & 0x0F) + (d->u[d->i+1] & 0x20) == 0) return 0; + } else if(ulen == 3) { + if((d->u[d->i] & 0x07) + (d->u[d->i+1] & 0x30) == 0) return 0; + } else if(ulen == 4) { + if((d->u[d->i] & 0x03) + (d->u[d->i+1] & 0x38) == 0) return 0; + } else if(ulen == 5) { + if((d->u[d->i] & 0x01) + (d->u[d->i+1] & 0x3C) == 0) return 0; + } + d->i += 1 + ulen; + } + } + +parse: + if(!has_escape) { + *value = enif_make_sub_binary(d->env, d->arg, st, (d->i - st - 1)); + return 1; + } + + hi = 0; + lo = 0; + + ulen = (d->i - 1) - st - num_escapes; + chrbuf = (char*) enif_make_new_binary(d->env, ulen, value); + chrpos = 0; + ui = st; + while(ui < d->i - 1) { + if(d->p[ui] != '\\') { + chrbuf[chrpos++] = d->p[ui++]; + continue; + } + ui++; + switch(d->p[ui]) { + case '\"': + case '\\': + case '/': + chrbuf[chrpos++] = d->p[ui]; + ui++; + break; + case 'b': + chrbuf[chrpos++] = '\b'; + ui++; + break; + case 'f': + chrbuf[chrpos++] = '\f'; + ui++; + break; + case 'n': + chrbuf[chrpos++] = '\n'; + ui++; + break; + case 'r': + chrbuf[chrpos++] = '\r'; + ui++; + break; + case 't': + chrbuf[chrpos++] = '\t'; + ui++; + break; + case 'u': + ui++; + hi = int_from_hex(&(d->u[ui])); + if(hi >= 0xD800 && hi < 0xDC00) { + lo = int_from_hex(&(d->u[ui+6])); + hi = utf8_from_pair(hi, lo); + ui += 10; + } else { + ui += 4; + } + hi = utf8_to_binary(hi, (unsigned char*) chrbuf+chrpos); + if(hi < 0) { + return 0; + } + chrpos += hi; + break; + default: + return 0; + } + } + + return 1; +} + +int +dec_number(Decoder* d, ERL_NIF_TERM* value) +{ + char state = nst_init; + char nbuf[NUM_BUF_LEN]; + int st = d->i; + int is_double = 0; + double dval; + long lval; + + while(d->i < d->len) { + switch(state) { + case nst_init: + switch(d->p[d->i]) { + case '-': + state = nst_sign; + d->i++; + break; + case '0': + state = nst_frac0; + d->i++; + break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + state = nst_mantissa; + d->i++; + break; + default: + return 0; + } + break; + + case nst_sign: + switch(d->p[d->i]) { + case '0': + state = nst_frac0; + d->i++; + break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + state = nst_mantissa; + d->i++; + break; + default: + return 0; + } + break; + + case nst_mantissa: + switch(d->p[d->i]) { + case '.': + state = nst_frac1; + d->i++; + break; + case 'e': + case 'E': + state = nst_esign; + d->i++; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + d->i++; + break; + default: + goto parse; + } + break; + + case nst_frac0: + switch(d->p[d->i]) { + case '.': + state = nst_frac1; + d->i++; + break; + case 'e': + case 'E': + state = nst_esign; + d->i++; + break; + default: + goto parse; + } + break; + + case nst_frac1: + is_double = 1; + switch(d->p[d->i]) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + state = nst_frac; + d->i++; + break; + default: + goto parse; + } + break; + + case nst_frac: + switch(d->p[d->i]) { + case 'e': + case 'E': + state = nst_esign; + d->i++; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + d->i++; + break; + default: + goto parse; + } + break; + + case nst_esign: + is_double = 1; + switch(d->p[d->i]) { + case '-': + case '+': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + state = nst_edigit; + d->i++; + break; + default: + return 0; + } + break; + + case nst_edigit: + switch(d->p[d->i]) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + d->i++; + break; + default: + goto parse; + } + break; + + default: + return 0; + } + } + +parse: + + switch(state) { + case nst_init: + case nst_sign: + case nst_frac1: + case nst_esign: + return 0; + default: + break; + } + + + if(st - d->i > NUM_BUF_LEN && is_double) { + return 0; + } else if(st - d->i > NUM_BUF_LEN) { + d->has_bignum = 1; + *value = enif_make_sub_binary(d->env, d->arg, st, d->i - st); + *value = enif_make_tuple2(d->env, d->atoms->atom_bignum, *value); + return 1; + } + + memset(nbuf, 0, NUM_BUF_LEN); + memcpy(nbuf, &(d->p[st]), d->i - st); + + errno = 0; + if(is_double) { + dval = strtod(nbuf, NULL); + if(errno == ERANGE) { + return 0; + } + *value = enif_make_double(d->env, dval); + return 1; + } + + lval = strtol(nbuf, NULL, 10); + if(errno == ERANGE) { + d->has_bignum = 1; + *value = enif_make_sub_binary(d->env, d->arg, st, d->i - st); + *value = enif_make_tuple2(d->env, d->atoms->atom_bignum, *value); + } else { + *value = enif_make_int64(d->env, lval); + } + return 1; +} + +ERL_NIF_TERM +make_object(ErlNifEnv* env, ERL_NIF_TERM pairs) +{ + ERL_NIF_TERM ret = enif_make_list(env, 0); + ERL_NIF_TERM key, val; + + while(enif_get_list_cell(env, pairs, &val, &pairs)) { + if(!enif_get_list_cell(env, pairs, &key, &pairs)) { + assert(0 == 1 && "Unbalanced object pairs."); + } + val = enif_make_tuple2(env, key, val); + ret = enif_make_list_cell(env, val, ret); + } + + return enif_make_tuple1(env, ret); +} + +ERL_NIF_TERM +make_array(ErlNifEnv* env, ERL_NIF_TERM list) +{ + ERL_NIF_TERM ret = enif_make_list(env, 0); + ERL_NIF_TERM item; + + while(enif_get_list_cell(env, list, &item, &list)) { + ret = enif_make_list_cell(env, item, ret); + } + + return ret; +} + +ERL_NIF_TERM +decode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + Decoder dec; + Decoder* d = &dec; + + ErlNifBinary bin; + + ERL_NIF_TERM objs = enif_make_list(env, 0); + ERL_NIF_TERM curr; + ERL_NIF_TERM val; + ERL_NIF_TERM ret; + + if(argc != 1) { + return enif_make_badarg(env); + } else if(!enif_inspect_binary(env, argv[0], &bin)) { + return enif_make_badarg(env); + } + + dec_init(d, env, argv[0], &bin); + + //fprintf(stderr, "Parsing:\r\n"); + while(d->i < bin.size) { + //fprintf(stderr, "state: %d\r\n", dec_curr(d)); + switch(dec_curr(d)) { + case st_value: + switch(d->p[d->i]) { + case ' ': + case '\n': + case '\r': + case '\t': + d->i++; + break; + case 'n': + if(d->i + 3 >= d->len) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + if(memcmp(&(d->p[d->i]), "null", 4) != 0) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + val = d->atoms->atom_null; + dec_pop(d, st_value); + d->i += 4; + break; + case 't': + if(d->i + 3 >= d->len) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + if(memcmp(&(d->p[d->i]), "true", 4) != 0) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + val = d->atoms->atom_true; + dec_pop(d, st_value); + d->i += 4; + break; + case 'f': + if(d->i + 4 >= bin.size) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + if(memcmp(&(d->p[d->i]), "false", 5) != 0) { + ret = dec_error(d, "invalid_literal"); + goto done; + } + val = d->atoms->atom_false; + dec_pop(d, st_value); + d->i += 5; + break; + case '\"': + if(!dec_string(d, &val)) { + ret = dec_error(d, "invalid_string"); + goto done; + } + dec_pop(d, st_value); + break; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if(!dec_number(d, &val)) { + ret = dec_error(d, "invalid_number"); + goto done; + } + dec_pop(d, st_value); + break; + case '{': + dec_push(d, st_object); + dec_push(d, st_key); + objs = enif_make_list_cell(env, curr, objs); + curr = enif_make_list(env, 0); + d->i++; + break; + case '[': + dec_push(d, st_array); + dec_push(d, st_value); + objs = enif_make_list_cell(env, curr, objs); + curr = enif_make_list(env, 0); + d->i++; + break; + case ']': + if(!enif_is_empty_list(env, curr)) { + ret = dec_error(d, "invalid_json"); + goto done; + } + dec_pop(d, st_value); + if(dec_curr(d) != st_array) { + ret = dec_error(d, "invalid_json"); + goto done; + } + dec_pop(d, st_array); + dec_pop(d, st_value); + val = curr; // curr is [] + if(!enif_get_list_cell(env, objs, &curr, &objs)) { + ret = dec_error(d, "internal_error"); + goto done; + } + d->i++; + break; + default: + ret = dec_error(d, "invalid_json"); + goto done; + } + if(dec_top(d) == 0) { + dec_push(d, st_done); + } else if(dec_curr(d) != st_value && dec_curr(d) != st_key) { + dec_push(d, st_comma); + curr = enif_make_list_cell(env, val, curr); + } + break; + + case st_key: + switch(d->p[d->i]) { + case ' ': + case '\n': + case '\r': + case '\t': + d->i++; + break; + case '\"': + if(!dec_string(d, &val)) { + ret = dec_error(d, "invalid_string"); + goto done; + } + dec_pop(d, st_key); + dec_push(d, st_colon); + curr = enif_make_list_cell(env, val, curr); + break; + case '}': + if(!enif_is_empty_list(env, curr)) { + ret = dec_error(d, "invalid_json"); + goto done; + } + dec_pop(d, st_key); + dec_pop(d, st_object); + dec_pop(d, st_value); + val = enif_make_tuple1(env, curr); + if(!enif_get_list_cell(env, objs, &curr, &objs)) { + ret = dec_error(d, "internal_error"); + goto done; + } + if(dec_top(d) == 0) { + dec_push(d, st_done); + } else { + dec_push(d, st_comma); + curr = enif_make_list_cell(env, val, curr); + } + d->i++; + break; + default: + ret = dec_error(d, "invalid_json"); + goto done; + } + break; + + case st_colon: + switch(d->p[d->i]) { + case ' ': + case '\n': + case '\r': + case '\t': + d->i++; + break; + case ':': + dec_pop(d, st_colon); + dec_push(d, st_value); + d->i++; + break; + default: + ret = dec_error(d, "invalid_json"); + goto done; + } + break; + + case st_comma: + switch(d->p[d->i]) { + case ' ': + case '\n': + case '\r': + case '\t': + d->i++; + break; + case ',': + dec_pop(d, st_comma); + switch(dec_curr(d)) { + case st_object: + dec_push(d, st_key); + break; + case st_array: + dec_push(d, st_value); + break; + default: + ret = dec_error(d, "internal_error"); + goto done; + } + d->i++; + break; + case '}': + dec_pop(d, st_comma); + if(dec_curr(d) != st_object) { + ret = dec_error(d, "invalid_json"); + goto done; + } + dec_pop(d, st_object); + dec_pop(d, st_value); + val = make_object(env, curr); + if(!enif_get_list_cell(env, objs, &curr, &objs)) { + ret = dec_error(d, "internal_error"); + goto done; + } + if(dec_top(d) > 0) { + dec_push(d, st_comma); + curr = enif_make_list_cell(env, val, curr); + } else { + dec_push(d, st_done); + } + d->i++; + break; + case ']': + dec_pop(d, st_comma); + if(dec_curr(d) != st_array) { + ret = dec_error(d, "invalid_json"); + goto done; + } + dec_pop(d, st_array); + dec_pop(d, st_value); + val = make_array(env, curr); + if(!enif_get_list_cell(env, objs, &curr, &objs)) { + ret = dec_error(d, "internal_error"); + goto done; + } + if(dec_top(d) > 0) { + dec_push(d, st_comma); + curr = enif_make_list_cell(env, val, curr); + } else { + dec_push(d, st_done); + } + d->i++; + break; + default: + ret = dec_error(d, "invalid_json"); + goto done; + } + break; + + case st_done: + switch(d->p[d->i]) { + case ' ': + case '\n': + case '\r': + case '\t': + d->i++; + break; + default: + ret = dec_error(d, "invalid_trailing_data"); + goto done; + } + break; + + default: + ret = dec_error(d, "invalid_internal_state"); + goto done; + } + } + + if(dec_curr(d) != st_done) { + ret = dec_error(d, "truncated_json"); + } else if(d->has_bignum) { + ret = enif_make_tuple2(env, d->atoms->atom_bignum, val); + } else { + ret = enif_make_tuple2(env, d->atoms->atom_ok, val); + } + +done: + dec_destroy(d); + + return ret; +} diff --git a/c_src/encoder.c b/c_src/encoder.c new file mode 100644 index 0000000..37dc822 --- /dev/null +++ b/c_src/encoder.c @@ -0,0 +1,556 @@ +#include +#include +#include + +#include "erl_nif.h" +#include "jiffy.h" + +#define BIN_INC_SIZE 1024 + +typedef struct { + ErlNifEnv* env; + jiffy_st* atoms; + + int count; + + ERL_NIF_TERM iolist; + ErlNifBinary curr; + int cleared; + + char* p; + unsigned char* u; + size_t i; +} Encoder; + +int +enc_init(Encoder* e, ErlNifEnv* env) +{ + e->env = env; + e->atoms = enif_priv_data(env); + + e->count = 0; + + e->iolist = enif_make_list(env, 0); + if(!enif_alloc_binary(BIN_INC_SIZE, &(e->curr))) { + return 0; + } + e->cleared = 0; + + e->p = (char*) e->curr.data; + e->u = (unsigned char*) e->curr.data; + e->i = 0; + + return 1; +} + +void +enc_destroy(Encoder* e) +{ + if(!e->cleared) { + enif_release_binary(&(e->curr)); + } +} + +ERL_NIF_TERM +enc_error(Encoder* e, const char* msg) +{ + assert(0 && msg); + return make_error(e->atoms, e->env, msg); +} + +int +enc_result(Encoder* e, ERL_NIF_TERM* value) +{ + if(e->i != e->curr.size) { + if(!enif_realloc_binary(&(e->curr), e->i)) { + return 0; + } + } + + *value = enif_make_binary(e->env, &(e->curr)); + e->cleared = 1; + return 1; +} + +int +enc_ensure(Encoder* e, size_t req) +{ + size_t new_sz; + + if(req < e->curr.size - e->i) { + return 1; + } + + new_sz = req - (e->curr.size - e->i); + new_sz += BIN_INC_SIZE - (new_sz % BIN_INC_SIZE); + assert(new_sz % BIN_INC_SIZE == 0 && "Invalid modulo math."); + + if(!enif_realloc_binary(&(e->curr), new_sz)) { + return 0; + } + + memset(&(e->u[e->i]), 0, e->curr.size - e->i); + + return 1; +} + +int +enc_literal(Encoder* e, const char* literal, size_t len) +{ + if(!enc_ensure(e, len)) { + return 0; + } + + memcpy(&(e->p[e->i]), literal, len); + e->i += len; + e->count++; + return 1; +} + +int +enc_string(Encoder* e, ERL_NIF_TERM val) +{ + ErlNifBinary bin; + char atom[512]; + + int esc_extra = 0; + int ulen; + int ui; + int i; + + if(enif_is_binary(e->env, val)) { + if(!enif_inspect_binary(e->env, val, &bin)) { + return 0; + } + } else if(enif_is_atom(e->env, val)) { + if(!enif_get_atom(e->env, val, atom, 512, ERL_NIF_LATIN1)) { + return 0; + } + // Fake as a binary for code below. + bin.data = (unsigned char*) atom; + bin.size = strlen(atom); + } else { + return 0; + } + + i = 0; + while(i < bin.size) { + switch((char) bin.data[i]) { + case '\"': + case '\\': + case '/': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + esc_extra += 1; + i++; + continue; + default: + if(bin.data[i] < 0x20) { + esc_extra += 5; + i++; + continue; + } else if(bin.data[i] < 0x80) { + i++; + continue; + } + ulen = -1; + if((bin.data[i] & 0xE0) == 0xC0) { + ulen = 1; + } else if((bin.data[i] & 0xF0) == 0xE0) { + ulen = 2; + } else if((bin.data[i] & 0xF8) == 0xF0) { + ulen = 3; + } else if((bin.data[i] & 0xFC) == 0xF8) { + ulen = 4; + } else if((bin.data[i] & 0xFE) == 0xFC) { + ulen = 5; + } + if(ulen < 0) { + return 0; + } + if(i+1+ulen > bin.size) { + return 0; + } + for(ui = 0; ui < ulen; ui++) { + if((bin.data[i+1+ui] & 0xC0) != 0x80) { + return 0; + } + } + if(ulen == 1) { + if((bin.data[i] & 0x1E) == 0) + return 0; + } else if(ulen == 2) { + if((bin.data[i] & 0x0F) + (bin.data[i+1] & 0x20) == 0) + return 0; + } else if(ulen == 3) { + if((bin.data[i] & 0x07) + (bin.data[i+1] & 0x30) == 0) + return 0; + } else if(ulen == 4) { + if((bin.data[i] & 0x03) + (bin.data[i+1] & 0x38) == 0) + return 0; + } else if(ulen == 5) { + if((bin.data[i] & 0x01) + (bin.data[i+1] & 0x3C) == 0) + return 0; + } + i += 1 + ulen; + } + } + + if(!enc_ensure(e, bin.size + esc_extra + 2)) { + return 0; + } + + e->p[e->i++] = '\"'; + + i = 0; + while(i < bin.size) { + switch((char) bin.data[i]) { + case '\"': + case '\\': + case '/': + e->p[e->i++] = '\\'; + e->u[e->i++] = bin.data[i]; + i++; + continue; + case '\b': + e->p[e->i++] = '\\'; + e->p[e->i++] = 'b'; + i++; + continue; + case '\f': + e->p[e->i++] = '\\'; + e->p[e->i++] = 'f'; + i++; + continue; + case '\n': + e->p[e->i++] = '\\'; + e->p[e->i++] = 'n'; + i++; + continue; + case '\r': + e->p[e->i++] = '\\'; + e->p[e->i++] = 'r'; + i++; + continue; + case '\t': + e->p[e->i++] = '\\'; + e->p[e->i++] = 't'; + i++; + continue; + default: + if(bin.data[i] < 0x20) { + e->p[e->i++] = '\\'; + e->p[e->i++] = 'u'; + if(!int_to_hex(bin.data[i], &(e->p[e->i]))) { + return 0; + } + e->i += 4; + i++; + } else { + e->u[e->i++] = bin.data[i++]; + } + } + } + + e->p[e->i++] = '\"'; + e->count++; + + return 1; +} + +int +enc_long(Encoder* e, long val) +{ + if(!enc_ensure(e, 32)) { + return 0; + } + + snprintf(&(e->p[e->i]), 32, "%ld", val); + e->i += strlen(&(e->p[e->i])); + e->count++; + + return 1; +} + +int +enc_double(Encoder* e, double val) +{ + if(!enc_ensure(e, 32)) { + return 0; + } + + snprintf(&(e->p[e->i]), 32, "%g", val); + e->i += strlen(&(e->p[e->i])); + e->count++; + + return 1; +} + +int +enc_char(Encoder* e, char c) +{ + if(!enc_ensure(e, 1)) { + return 0; + } + + e->p[e->i++] = c; + return 1; +} + +int +enc_start_object(Encoder* e) +{ + e->count++; + return enc_char(e, '{'); +} + +int +enc_end_object(Encoder* e) +{ + return enc_char(e, '}'); +} + +int +enc_start_array(Encoder* e) +{ + e->count++; + return enc_char(e, '['); +} + +int +enc_end_array(Encoder* e) +{ + return enc_char(e, ']'); +} + +int +enc_colon(Encoder* e) +{ + return enc_char(e, ':'); +} + +int +enc_comma(Encoder* e) +{ + return enc_char(e, ','); +} + +ERL_NIF_TERM +encode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + Encoder enc; + Encoder* e = &enc; + + ERL_NIF_TERM ret; + + ERL_NIF_TERM stack; + ERL_NIF_TERM curr; + ERL_NIF_TERM item; + const ERL_NIF_TERM* tuple; + int arity; + double dval; + long lval; + + int has_unknown = 0; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enc_init(e, env)) { + return enif_make_badarg(env); + } + + stack = enif_make_list1(env, argv[0]); + + while(!enif_is_empty_list(env, stack)) { + if(!enif_get_list_cell(env, stack, &curr, &stack)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(enif_is_identical(curr, e->atoms->ref_object)) { + if(!enif_get_list_cell(env, stack, &curr, &stack)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(enif_is_empty_list(env, curr)) { + if(!enc_end_object(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + continue; + } + if(!enif_get_list_cell(env, curr, &item, &curr)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(!enif_get_tuple(env, item, &arity, &tuple)) { + ret = enc_error(e, "invalid_object_pair"); + goto done; + } + if(arity != 2) { + ret = enc_error(e, "invalid_object_pair"); + goto done; + } + if(!enc_comma(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(!enc_string(e, tuple[0])) { + ret = enc_error(e, "invalid_object_key"); + goto done; + } + if(!enc_colon(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + stack = enif_make_list_cell(env, curr, stack); + stack = enif_make_list_cell(env, e->atoms->ref_object, stack); + stack = enif_make_list_cell(env, tuple[1], stack); + } else if(enif_is_identical(curr, e->atoms->ref_array)) { + if(!enif_get_list_cell(env, stack, &curr, &stack)) { + ret = enc_error(e, "internal_error.5"); + goto done; + } + if(enif_is_empty_list(env, curr)) { + if(!enc_end_array(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + continue; + } + if(!enc_comma(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(!enif_get_list_cell(env, curr, &item, &curr)) { + ret = enc_error(e, "internal_error"); + goto done; + } + stack = enif_make_list_cell(env, curr, stack); + stack = enif_make_list_cell(env, e->atoms->ref_array, stack); + stack = enif_make_list_cell(env, item, stack); + } else if(enif_compare(curr, e->atoms->atom_null) == 0) { + if(!enc_literal(e, "null", 4)) { + ret = enc_error(e, "null"); + goto done; + } + } else if(enif_compare(curr, e->atoms->atom_true) == 0) { + if(!enc_literal(e, "true", 4)) { + ret = enc_error(e, "true"); + goto done; + } + } else if(enif_compare(curr, e->atoms->atom_false) == 0) { + if(!enc_literal(e, "false", 5)) { + ret = enc_error(e, "false"); + goto done; + } + } else if(enif_is_binary(env, curr)) { + if(!enc_string(e, curr)) { + ret = enc_error(e, "invalid_string"); + goto done; + } + } else if(enif_is_atom(env, curr)) { + if(!enc_string(e, curr)) { + ret = enc_error(e, "invalid_string"); + goto done; + } + } else if(enif_get_int64(env, curr, &lval)) { + if(!enc_long(e, lval)) { + ret = enc_error(e, "internal_error"); + goto done; + } + } else if(enif_get_double(env, curr, &dval)) { + if(!enc_double(e, dval)) { + ret = enc_error(e, "internal_error"); + goto done; + } + } else if(enif_get_tuple(env, curr, &arity, &tuple)) { + if(arity != 1) { + ret = enc_error(e, "invalid_ejson"); + goto done; + } + if(!enif_is_list(env, tuple[0])) { + ret = enc_error(e, "invalid_object"); + goto done; + } + if(!enc_start_object(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(enif_is_empty_list(env, tuple[0])) { + if(!enc_end_object(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + continue; + } + if(!enif_get_list_cell(env, tuple[0], &item, &curr)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(!enif_get_tuple(env, item, &arity, &tuple)) { + ret = enc_error(e, "invalid_object_pair"); + goto done; + } + if(arity != 2) { + ret = enc_error(e, "invalid_object_pair"); + goto done; + } + if(!enc_string(e, tuple[0])) { + ret = enc_error(e, "invalid_object_key"); + goto done; + } + if(!enc_colon(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + stack = enif_make_list_cell(env, curr, stack); + stack = enif_make_list_cell(env, e->atoms->ref_object, stack); + stack = enif_make_list_cell(env, tuple[1], stack); + } else if(enif_is_list(env, curr)) { + if(!enc_start_array(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + if(enif_is_empty_list(env, curr)) { + if(!enc_end_array(e)) { + ret = enc_error(e, "internal_error"); + goto done; + } + continue; + } + if(!enif_get_list_cell(env, curr, &item, &curr)) { + ret = enc_error(e, "internal_error"); + goto done; + } + stack = enif_make_list_cell(env, curr, stack); + stack = enif_make_list_cell(env, e->atoms->ref_array, stack); + stack = enif_make_list_cell(env, item, stack); + } else { + has_unknown = 1; + ret = enc_error(e, "invalid_ejson"); + goto done; + /* + if(!enc_unknown(env, curr)) { + ret = enc_error(e, "internal_error"); + goto done; + } + */ + } + } while(!enif_is_empty_list(env, stack)); + + if(!enc_result(e, &item)) { + ret = enc_error(e, "internal_error"); + goto done; + } + + ret = enif_make_tuple2(env, e->atoms->atom_ok, item); + +done: + enc_destroy(e); + return ret; +} diff --git a/c_src/jiffy.c b/c_src/jiffy.c new file mode 100644 index 0000000..13e2979 --- /dev/null +++ b/c_src/jiffy.c @@ -0,0 +1,52 @@ +#include "jiffy.h" + +static int +load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) +{ + jiffy_st* st = enif_alloc(sizeof(jiffy_st)); + if(st == NULL) { + return 1; + } + + st->atom_ok = make_atom(env, "ok"); + st->atom_error = make_atom(env, "error"); + st->atom_null = make_atom(env, "null"); + st->atom_true = make_atom(env, "true"); + st->atom_false = make_atom(env, "false"); + st->atom_bignum = make_atom(env, "bignum"); + + st->ref_object = enif_make_ref(env); + st->ref_array = enif_make_ref(env); + + *priv = (void*) st; + + return 0; +} + +static int +reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) +{ + return 0; +} + +static int +upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) +{ + *priv = *old_priv; + return 0; +} + +static void +unload(ErlNifEnv* env, void* priv) +{ + enif_free(priv); + return; +} + +static ErlNifFunc funcs[] = +{ + {"nif_decode", 1, decode}, + {"nif_encode", 1, encode} +}; + +ERL_NIF_INIT(jiffy, funcs, &load, &reload, &upgrade, &unload); diff --git a/c_src/jiffy.h b/c_src/jiffy.h new file mode 100644 index 0000000..7fa0b81 --- /dev/null +++ b/c_src/jiffy.h @@ -0,0 +1,30 @@ +#ifndef JIFFY_H +#define JIFFY_H + +#include "erl_nif.h" + +typedef struct { + ERL_NIF_TERM atom_ok; + ERL_NIF_TERM atom_error; + ERL_NIF_TERM atom_null; + ERL_NIF_TERM atom_true; + ERL_NIF_TERM atom_false; + ERL_NIF_TERM atom_bignum; + ERL_NIF_TERM ref_object; + ERL_NIF_TERM ref_array; +} jiffy_st; + +ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name); +ERL_NIF_TERM make_ok(jiffy_st* st, ErlNifEnv* env, ERL_NIF_TERM data); +ERL_NIF_TERM make_error(jiffy_st* st, ErlNifEnv* env, const char* error); + +ERL_NIF_TERM decode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +ERL_NIF_TERM encode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); + +int int_from_hex(const unsigned char* p); +int int_to_hex(int val, char* p); +int utf8_len(int c); +int utf8_from_pair(int hi, int lo); +int utf8_to_binary(int c, unsigned char* buf); + +#endif // Included JIFFY_H diff --git a/c_src/utf8.c b/c_src/utf8.c new file mode 100644 index 0000000..280229c --- /dev/null +++ b/c_src/utf8.c @@ -0,0 +1,128 @@ + +static const char hexvals[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 +}; + +static const char hexdigits[16] = { + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F' +}; + +int +int_from_hex(const unsigned char* p) +{ + unsigned char* h = (unsigned char*) p; + int ret; + + if(hexvals[*(h+0)] < 0) return -1; + if(hexvals[*(h+1)] < 0) return -1; + if(hexvals[*(h+2)] < 0) return -1; + if(hexvals[*(h+3)] < 0) return -1; + + ret = (hexvals[*(h+0)] << 12) + + (hexvals[*(h+1)] << 8) + + (hexvals[*(h+2)] << 4) + + (hexvals[*(h+3)] << 0); + + return ret; +} + +int +int_to_hex(int val, char* p) +{ + if(val < 0 || val > 65535) + return 0; + + p[0] = hexdigits[(val >> 12) & 0xF]; + p[1] = hexdigits[(val >> 8) & 0xF]; + p[2] = hexdigits[(val >> 4) & 0xF]; + p[3] = hexdigits[val & 0xF]; + + return 1; +} + +int +utf8_len(int c) +{ + if(c < 128) { + return 1; + } else if(c < 0x800) { + return 2; + } else if(c < 0x10000) { + if(c < 0xD800 || (c > 0xDFFF && c < 0xFFFE)) { + return 3; + } else { + return -1; + } + } else if(c < 0x200000) { + return 4; + } else if(c < 0x4000000) { + return 5; + } else if(c < 0x80000000) { + return 6; + } else { + return -1; + } +} + +int +utf8_from_pair(int hi, int lo) +{ + if(hi < 0xD800 || hi >= 0xDC00) return -1; + if(lo < 0xDC00 || lo > 0xDFFF) return -1; + return ((hi & 0x3FF) << 10) + (lo & 0x3FF) + 0x10000; +} + +int +utf8_to_binary(int c, unsigned char* buf) +{ + if(c < 0x80) { + buf[0] = (unsigned char) c; + return 1; + } else if(c < 0x800) { + buf[0] = (unsigned char) 0xC0 + (c >> 6); + buf[1] = (unsigned char) 0x80 + (c & 0x3F); + return 2; + } else if(c < 0x10000) { + if(c < 0xD800 || (c > 0xDFFF && c < 0xFFFE)) { + buf[0] = (unsigned char) 0xE0 + (c >> 12); + buf[1] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); + buf[2] = (unsigned char) 0x80 + (c & 0x3F); + return 3; + } else { + return -1; + } + } else if(c < 0x200000) { + buf[0] = (unsigned char) 0xF0 + (c >> 18); + buf[1] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); + buf[2] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); + buf[3] = (unsigned char) 0x80 + (c & 0x3F); + return 4; + } else if(c < 0x4000000) { + buf[0] = (unsigned char) 0xF8 + (c >> 24); + buf[1] = (unsigned char) 0x80 + ((c >> 18) & 0x3F); + buf[2] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); + buf[3] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); + buf[4] = (unsigned char) 0x80 + (c & 0x3F); + return 5; + } else if(c < 0x80000000) { + buf[0] = (unsigned char) 0xFC + (c >> 30); + buf[1] = (unsigned char) 0x80 + ((c >> 24) & 0x3F); + buf[2] = (unsigned char) 0x80 + ((c >> 18) & 0x3F); + buf[3] = (unsigned char) 0x80 + ((c >> 12) & 0x3F); + buf[4] = (unsigned char) 0x80 + ((c >> 6) & 0x3F); + buf[5] = (unsigned char) 0x80 + (c & 0x3F); + return 6; + } + return -1; +} + diff --git a/c_src/util.c b/c_src/util.c new file mode 100644 index 0000000..eaf7afe --- /dev/null +++ b/c_src/util.c @@ -0,0 +1,23 @@ +#include "jiffy.h" + +ERL_NIF_TERM +make_atom(ErlNifEnv* env, const char* name) +{ + ERL_NIF_TERM ret; + if(enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { + return ret; + } + return enif_make_atom(env, name); +} + +ERL_NIF_TERM +make_ok(jiffy_st* st, ErlNifEnv* env, ERL_NIF_TERM value) +{ + return enif_make_tuple2(env, st->atom_ok, value); +} + +ERL_NIF_TERM +make_error(jiffy_st* st, ErlNifEnv* env, const char* error) +{ + return enif_make_tuple2(env, st->atom_error, make_atom(env, error)); +} diff --git a/rebar b/rebar new file mode 100755 index 0000000..c55b6e7 Binary files /dev/null and b/rebar differ diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..081bf89 --- /dev/null +++ b/rebar.config @@ -0,0 +1,17 @@ +{port_sources, ["c_src/*.c"]}. +{so_name, "jiffy.so"}. +{port_envs, [ + %% Link the spidermonkey library + {".*", "CFLAGS", "$CFLAGS -g -Wall"}, + + %% Make sure to link -lstdc++ on linux or solaris + {"(linux|solaris)", "LDFLAGS", "$LDFLAGS -lstdc++"}, + + %% OS X Leopard flags for 64-bit + {"darwin9.*-64$", "CXXFLAGS", "-m64"}, + {"darwin9.*-64$", "LDFLAGS", "-arch x86_64"}, + + %% OS X Snow Leopard flags for 32-bit + {"darwin10.*-32$", "CXXFLAGS", "-m32"}, + {"darwin10.*-32$", "LDFLAGS", "-arch i386"} +]}. diff --git a/src/jiffy.app.src b/src/jiffy.app.src new file mode 100644 index 0000000..32adefa --- /dev/null +++ b/src/jiffy.app.src @@ -0,0 +1,7 @@ +{application, jiffy, [ + {description, "JSON Decoder/Encoder."}, + {vsn, "%VSN%"}, + {registered, []}, + {mod, reloading, []}, + {applications, [kernel]} +]}. diff --git a/src/jiffy.erl b/src/jiffy.erl new file mode 100644 index 0000000..144980b --- /dev/null +++ b/src/jiffy.erl @@ -0,0 +1,62 @@ +-module(jiffy). +-export([decode/1, encode/1]). +-define(NOT_LOADED, not_loaded(?LINE)). + +-on_load(init/0). + +decode(Data) -> + case nif_decode(Data) of + {bignum, EJson} -> + {ok, debignum(EJson)}; + Else -> + Else + end. + +encode(Data) -> + nif_encode(Data). + + +nif_decode(_Data) -> + ?NOT_LOADED. + +nif_encode(_Data) -> + ?NOT_LOADED. + + +debignum({bignum, Value}) -> + list_to_integer(binary_to_list(Value)); +debignum({Pairs}) when is_list(Pairs) -> + debignum_obj(Pairs, []); +debignum(Vals) when is_list(Vals) -> + debignum_arr(Vals, []); +debignum(Val) -> + Val. + +debignum_obj([], Acc) -> + {lists:reverse(Acc)}; +debignum_obj([{K, V} | Pairs], Acc) -> + debignum_obj(Pairs, [{K, debignum(V)} | Acc]). + +debignum_arr([], Acc) -> + lists:reverse(Acc); +debignum_arr([V | Vals], Acc) -> + debignum_arr(Vals, [debignum(V) | Acc]). + + +init() -> + SoName = case code:priv_dir(?MODULE) of + {error, bad_name} -> + case filelib:is_dir(filename:join(["..", priv])) of + true -> + filename:join(["..", priv, ?MODULE]); + _ -> + filename:join([priv, ?MODULE]) + end; + Dir -> + filename:join(Dir, ?MODULE) + end, + erlang:load_nif(SoName, 0). + + +not_loaded(Line) -> + exit({not_loaded, [{module, ?MODULE}, {line, Line}]}). diff --git a/test/001-yajl-tests.t b/test/001-yajl-tests.t new file mode 100755 index 0000000..0f11afb --- /dev/null +++ b/test/001-yajl-tests.t @@ -0,0 +1,33 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("test"), + code:add_pathz("ebin"), + + Cases = read_cases(), + + etap:plan(length(Cases)), + lists:foreach(fun(Case) -> test(Case) end, Cases), + etap:end_tests(). + +test({Name, Json, Erl}) -> + etap:is(jiffy:decode(Json), Erl, Name). + +read_cases() -> + CasesPath = filename:join(["test", "cases", "*.json"]), + FileNames = lists:sort(filelib:wildcard(CasesPath)), + lists:map(fun(F) -> make_pair(F) end, FileNames). + +make_pair(FileName) -> + {ok, Json} = file:read_file(FileName), + {BaseName, _} = lists:splitwith(fun(C) -> C /= $. end, FileName), + ErlFname = BaseName ++ ".erl", + {ok, [Term]} = file:consult(ErlFname), + case Term of + {error, _} -> + {BaseName, Json, Term}; + {error, _, _} -> + {BaseName, Json, Term}; + _ -> + {BaseName, Json, {ok, Term}} + end. diff --git a/test/002-literals.t b/test/002-literals.t new file mode 100755 index 0000000..a49caeb --- /dev/null +++ b/test/002-literals.t @@ -0,0 +1,19 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(6), + etap:is(jiffy:decode(<<"true">>), {ok, true}, "DEC: true -> true"), + etap:is(jiffy:encode(true), {ok, <<"true">>}, "ENC: true -> true"), + + etap:is(jiffy:decode(<<"false">>), {ok, false}, "DEC: false -> false"), + etap:is(jiffy:encode(false), {ok, <<"false">>}, "ENC: false -> false"), + + etap:is(jiffy:decode(<<"null">>), {ok, null}, "DEC: null -> null"), + etap:is(jiffy:encode(null), {ok, <<"null">>}, "ENC: null -> null"), + + etap:end_tests(). + + diff --git a/test/003-numbers.t b/test/003-numbers.t new file mode 100755 index 0000000..a596725 --- /dev/null +++ b/test/003-numbers.t @@ -0,0 +1,47 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(47), + util:test_good(good()), + util:test_errors(errors()), + etap:end_tests(). + +good() -> + [ + {<<"0">>, 0}, + {<<"-0">>, 0, <<"0">>}, + {<<"1">>, 1}, + {<<"12">>, 12}, + {<<"-3">>, -3}, + {<<"309230948234098">>, 309230948234098}, + {<<"1.0">>, 1.0, <<"1">>}, + {<<"0.3">>, 0.3}, + {<<"2.4234324">>, 2.4234324, <<"2.42343">>}, + {<<"-3.1416">>, -3.1416}, + {<<"1E4">>, 10000.0, <<"10000">>}, + {<<"1.0E+01">>, 10.0, <<"10">>}, + {<<"1e1">>, 10.0, <<"10">>}, + {<<"3.0E2">>, 300.0, <<"300">>}, + {<<"0E3">>, 0.0, <<"0">>}, + {<<"1.5E3">>, 1500.0, <<"1500">>}, + {<<"1.5E-1">>, 0.15, <<"0.15">>}, + {<<"-0.323E+2">>, -32.3, <<"-32.3">>} + ]. + +errors() -> + [ + <<"02">>, + <<"-01">>, + <<"+12">>, + <<"-">>, + <<"1.">>, + <<".1">>, + <<"1.-1">>, + <<"1E">>, + <<"1-E2">>, + <<"2E +3">>, + <<"1EA">> + ]. diff --git a/test/004-strings.t b/test/004-strings.t new file mode 100755 index 0000000..ec5f568 --- /dev/null +++ b/test/004-strings.t @@ -0,0 +1,36 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(21), + util:test_good(good()), + util:test_errors(errors()), + etap:end_tests(). + +good() -> + [ + {<<"\"\"">>, <<"">>}, + {<<"\"0\"">>, <<"0">>}, + {<<"\"foo\"">>, <<"foo">>}, + {<<"\"\\\"foobar\\\"\"">>, <<"\"foobar\"">>}, + {<<"\"\\n\\n\\n\"">>, <<"\n\n\n">>}, + {<<"\"\\\" \\b\\f\\r\\n\\t\\\"\"">>, <<"\" \b\f\r\n\t\"">>}, + {<<"\"foo\\u0005bar\"">>, <<"foo", 5, "bar">>}, + { + <<"\"\\uD834\\uDD1E\"">>, + <<240, 157, 132, 158>>, + <<34, 240, 157, 132, 158, 34>> + } + ]. + +errors() -> + [ + <<"\"", 0, "\"">>, + <<"\"\\g\"">>, + <<"\"\\uFFFF\"">>, + <<"\"\\uD834foo\\uDD1E\"">>, + % CouchDB-345 + <<"\"",78,69,73,77,69,78,32,70,216,82,82,32,70,65,69,78,33,"\"">> + ]. diff --git a/test/005-arrays.t b/test/005-arrays.t new file mode 100755 index 0000000..0a0dd5e --- /dev/null +++ b/test/005-arrays.t @@ -0,0 +1,34 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(18), + util:test_good(good()), + util:test_errors(errors()), + etap:end_tests(). + +good() -> + [ + {<<"[]">>, []}, + {<<"[\t[\n]\r]">>, [[]], <<"[[]]">>}, + {<<"[\t123, \r true\n]">>, [123, true], <<"[123,true]">>}, + {<<"[1,\"foo\"]">>, [1, <<"foo">>]}, + {<<"[1199344435545.0,1]">>, [1199344435545.0,1], <<"[1.19934e+12,1]">>}, + { + <<"[\"\\u00A1\",\"\\u00FC\"]">>, + [<<194, 161>>, <<195, 188>>], + <<"[\"", 194, 161, "\",\"", 195, 188, "\"]">> + } + ]. + +errors() -> + [ + <<"[">>, + <<"]">>, + <<"[,]">>, + <<"[123">>, + <<"[123,]">>, + <<"[32 true]">> + ]. diff --git a/test/006-maps.t b/test/006-maps.t new file mode 100755 index 0000000..ebb8588 --- /dev/null +++ b/test/006-maps.t @@ -0,0 +1,34 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(15), + util:test_good(good()), + util:test_errors(errors()), + etap:end_tests(). + +good() -> + [ + {<<"{}">>, {[]}}, + {<<"{\"foo\": \"bar\"}">>, + {[{<<"foo">>, <<"bar">>}]}, + <<"{\"foo\":\"bar\"}">>}, + {<<"\n\n{\"foo\":\r \"bar\",\n \"baz\"\t: 123 }">>, + {[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, + <<"{\"foo\":\"bar\",\"baz\":123}">>} + ]. + +errors() -> + [ + <<"{">>, + <<"{,}">>, + <<"{123:true}">>, + <<"{false:123}">>, + <<"{:\"stuff\"}">>, + <<"{\"key\":}">>, + <<"{\"key\": 123">>, + <<"{\"key\": 123 true">>, + <<"{\"key\": 123,}">> + ]. diff --git a/test/007-compound.t b/test/007-compound.t new file mode 100755 index 0000000..849f844 --- /dev/null +++ b/test/007-compound.t @@ -0,0 +1,39 @@ +#! /usr/bin/env escript + +main([]) -> + code:add_pathz("ebin"), + code:add_pathz("test"), + + etap:plan(12), + util:test_good(good()), + util:test_errors(errors()), + etap:end_tests(). + +good() -> + [ + {<<"[{}]">>, [{[]}]}, + {<<"{\"foo\":[123]}">>, {[{<<"foo">>, [123]}]}}, + {<<"{\"foo\":{\"bar\":true}}">>, + {[{<<"foo">>, {[{<<"bar">>, true}]} }]} }, + {<<"{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}">>, + {[ + {<<"foo">>, []}, + {<<"bar">>, {[{<<"baz">>, true}]}}, + {<<"alice">>, <<"bob">>} + ]} + }, + {<<"[-123,\"foo\",{\"bar\":[]},null]">>, + [ + -123, + <<"foo">>, + {[{<<"bar">>, []}]}, + null + ] + } + ]. + +errors() -> + [ + <<"[{}">>, + <<"}]">> + ]. diff --git a/test/cases/array.erl b/test/cases/array.erl new file mode 100644 index 0000000..8a9688e --- /dev/null +++ b/test/cases/array.erl @@ -0,0 +1,16 @@ +[ + <<"foo">>, + <<"bar">>, + <<"baz">>, + true, + false, + null, + {[{<<"key">>, <<"value">>}]}, + [ + null, + null, + null, + [] + ], + <<"\n\r\\">> +]. \ No newline at end of file diff --git a/test/cases/array.json b/test/cases/array.json new file mode 100644 index 0000000..f76058d --- /dev/null +++ b/test/cases/array.json @@ -0,0 +1,6 @@ +["foo", + "bar", "baz", + true,false,null,{"key":"value"}, + [null,null,null,[]], + "\n\r\\" +] diff --git a/test/cases/array_close.erl b/test/cases/array_close.erl new file mode 100644 index 0000000..4aa294e --- /dev/null +++ b/test/cases/array_close.erl @@ -0,0 +1 @@ +{error,{1,invalid_json}}. diff --git a/test/cases/array_close.json b/test/cases/array_close.json new file mode 100644 index 0000000..079b579 --- /dev/null +++ b/test/cases/array_close.json @@ -0,0 +1 @@ +] diff --git a/test/cases/array_open.erl b/test/cases/array_open.erl new file mode 100644 index 0000000..9fa573a --- /dev/null +++ b/test/cases/array_open.erl @@ -0,0 +1 @@ +{error,{3,truncated_json}}. diff --git a/test/cases/array_open.json b/test/cases/array_open.json new file mode 100644 index 0000000..558ed37 --- /dev/null +++ b/test/cases/array_open.json @@ -0,0 +1 @@ +[ diff --git a/test/cases/bogus_char.erl b/test/cases/bogus_char.erl new file mode 100644 index 0000000..9ecbac7 --- /dev/null +++ b/test/cases/bogus_char.erl @@ -0,0 +1 @@ +{error,{97,invalid_literal}}. diff --git a/test/cases/bogus_char.json b/test/cases/bogus_char.json new file mode 100644 index 0000000..8163bd8 --- /dev/null +++ b/test/cases/bogus_char.json @@ -0,0 +1,4 @@ +["this","is","what","should","be", + "a happy bit of json", + "but someone, misspelled \"true\"", ture, + "who says JSON is easy for humans to generate?"] diff --git a/test/cases/codepoints_from_unicode_org.erl b/test/cases/codepoints_from_unicode_org.erl new file mode 100644 index 0000000..086d058 --- /dev/null +++ b/test/cases/codepoints_from_unicode_org.erl @@ -0,0 +1 @@ +<<77, 208, 176, 228, 186, 140, 240, 144, 140, 130>>. \ No newline at end of file diff --git a/test/cases/codepoints_from_unicode_org.json b/test/cases/codepoints_from_unicode_org.json new file mode 100644 index 0000000..f91f3be --- /dev/null +++ b/test/cases/codepoints_from_unicode_org.json @@ -0,0 +1 @@ +"\u004d\u0430\u4e8c\ud800\udf02" diff --git a/test/cases/deep_arrays.erl b/test/cases/deep_arrays.erl new file mode 100644 index 0000000..2721754 --- /dev/null +++ b/test/cases/deep_arrays.erl @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]. diff --git a/test/cases/deep_arrays.json b/test/cases/deep_arrays.json new file mode 100644 index 0000000..82d1b0d --- /dev/null +++ b/test/cases/deep_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/test/cases/difficult_json_c_test_case.erl b/test/cases/difficult_json_c_test_case.erl new file mode 100644 index 0000000..591fafe --- /dev/null +++ b/test/cases/difficult_json_c_test_case.erl @@ -0,0 +1,19 @@ +{[ + {<<"glossary">>, {[ + {<<"title">>, <<"example glossary">>}, + {<<"GlossDiv">>, {[ + {<<"title">>, <<"S">>}, + {<<"GlossList">>, [ + {[ + {<<"ID">>, <<"SGML">>}, + {<<"SortAs">>, <<"SGML">>}, + {<<"GlossTerm">>, <<"Standard Generalized Markup Language">>}, + {<<"Acronym">>, <<"SGML">>}, + {<<"Abbrev">>, <<"ISO 8879:1986">>}, + {<<"GlossDef">>, <<"A meta-markup language, used to create markup languages such as DocBook.">>}, + {<<"GlossSeeAlso">>, [<<"GML">>, <<"XML">>, <<"markup">>]} + ]} + ]} + ]}} + ]}} +]}. diff --git a/test/cases/difficult_json_c_test_case.json b/test/cases/difficult_json_c_test_case.json new file mode 100644 index 0000000..6998f55 --- /dev/null +++ b/test/cases/difficult_json_c_test_case.json @@ -0,0 +1 @@ +{ "glossary": { "title": "example glossary", "GlossDiv": { "title": "S", "GlossList": [ { "ID": "SGML", "SortAs": "SGML", "GlossTerm": "Standard Generalized Markup Language", "Acronym": "SGML", "Abbrev": "ISO 8879:1986", "GlossDef": "A meta-markup language, used to create markup languages such as DocBook.", "GlossSeeAlso": ["GML", "XML", "markup"] } ] } } } diff --git a/test/cases/doubles.erl b/test/cases/doubles.erl new file mode 100644 index 0000000..76a7f07 --- /dev/null +++ b/test/cases/doubles.erl @@ -0,0 +1 @@ +[10, 10, 3.141569, 1000]. diff --git a/test/cases/doubles.json b/test/cases/doubles.json new file mode 100644 index 0000000..626f21c --- /dev/null +++ b/test/cases/doubles.json @@ -0,0 +1 @@ +[ 0.1e2, 1e1, 3.141569, 10000000000000e-10] diff --git a/test/cases/empty_array.erl b/test/cases/empty_array.erl new file mode 100644 index 0000000..2600237 --- /dev/null +++ b/test/cases/empty_array.erl @@ -0,0 +1 @@ +[]. \ No newline at end of file diff --git a/test/cases/empty_array.json b/test/cases/empty_array.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/test/cases/empty_array.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/cases/empty_string.erl b/test/cases/empty_string.erl new file mode 100644 index 0000000..e0f2023 --- /dev/null +++ b/test/cases/empty_string.erl @@ -0,0 +1 @@ +<<"">>. \ No newline at end of file diff --git a/test/cases/empty_string.json b/test/cases/empty_string.json new file mode 100644 index 0000000..e16c76d --- /dev/null +++ b/test/cases/empty_string.json @@ -0,0 +1 @@ +"" diff --git a/test/cases/escaped_bulgarian.erl b/test/cases/escaped_bulgarian.erl new file mode 100644 index 0000000..15287cc --- /dev/null +++ b/test/cases/escaped_bulgarian.erl @@ -0,0 +1,6 @@ +[ + <<208, 148, 208, 176>>, + <<208, 156, 209, 131>>, + <<208, 149, 208, 177, 208, 176>>, + <<208, 156, 208, 176, 208, 185, 208, 186, 208, 176, 209, 130, 208, 176>> +]. \ No newline at end of file diff --git a/test/cases/escaped_bulgarian.json b/test/cases/escaped_bulgarian.json new file mode 100644 index 0000000..9ce1d1c --- /dev/null +++ b/test/cases/escaped_bulgarian.json @@ -0,0 +1,4 @@ +["\u0414\u0430", + "\u041c\u0443", + "\u0415\u0431\u0430", + "\u041c\u0430\u0439\u043a\u0430\u0442\u0430"] diff --git a/test/cases/escaped_foobar.erl b/test/cases/escaped_foobar.erl new file mode 100644 index 0000000..de9c37d --- /dev/null +++ b/test/cases/escaped_foobar.erl @@ -0,0 +1 @@ +<<"foobar">>. \ No newline at end of file diff --git a/test/cases/escaped_foobar.json b/test/cases/escaped_foobar.json new file mode 100644 index 0000000..2c0e25f --- /dev/null +++ b/test/cases/escaped_foobar.json @@ -0,0 +1 @@ +"\u0066\u006f\u006f\u0062\u0061\u0072" diff --git a/test/cases/false.erl b/test/cases/false.erl new file mode 100644 index 0000000..aca5de7 --- /dev/null +++ b/test/cases/false.erl @@ -0,0 +1 @@ +false. \ No newline at end of file diff --git a/test/cases/false.json b/test/cases/false.json new file mode 100644 index 0000000..c508d53 --- /dev/null +++ b/test/cases/false.json @@ -0,0 +1 @@ +false diff --git a/test/cases/false_then_garbage.erl b/test/cases/false_then_garbage.erl new file mode 100644 index 0000000..fc13df2 --- /dev/null +++ b/test/cases/false_then_garbage.erl @@ -0,0 +1 @@ +{error,{6,invalid_trailing_data}}. diff --git a/test/cases/false_then_garbage.json b/test/cases/false_then_garbage.json new file mode 100644 index 0000000..78f4e96 --- /dev/null +++ b/test/cases/false_then_garbage.json @@ -0,0 +1 @@ +falsex \ No newline at end of file diff --git a/test/cases/four_byte_utf8.erl b/test/cases/four_byte_utf8.erl new file mode 100644 index 0000000..20da680 --- /dev/null +++ b/test/cases/four_byte_utf8.erl @@ -0,0 +1 @@ +{[{<<"U+10ABCD">>, <<244, 138, 175, 141>>}]}. diff --git a/test/cases/four_byte_utf8.json b/test/cases/four_byte_utf8.json new file mode 100644 index 0000000..582c575 --- /dev/null +++ b/test/cases/four_byte_utf8.json @@ -0,0 +1,2 @@ +{ "U+10ABCD": "􊯍" } + diff --git a/test/cases/integers.erl b/test/cases/integers.erl new file mode 100644 index 0000000..bd642e4 --- /dev/null +++ b/test/cases/integers.erl @@ -0,0 +1,13 @@ +[ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 123456789, + -123456789, + 2147483647, + -2147483647 +]. \ No newline at end of file diff --git a/test/cases/integers.json b/test/cases/integers.json new file mode 100644 index 0000000..c50e4c9 --- /dev/null +++ b/test/cases/integers.json @@ -0,0 +1,3 @@ +[ 1,2,3,4,5,6,7, + 123456789 , -123456789, + 2147483647, -2147483647 ] diff --git a/test/cases/invalid_utf8.erl b/test/cases/invalid_utf8.erl new file mode 100644 index 0000000..f0baaf0 --- /dev/null +++ b/test/cases/invalid_utf8.erl @@ -0,0 +1 @@ +{error,{10,invalid_string}}. diff --git a/test/cases/invalid_utf8.json b/test/cases/invalid_utf8.json new file mode 100644 index 0000000..12f1718 --- /dev/null +++ b/test/cases/invalid_utf8.json @@ -0,0 +1 @@ +["Да М Еба Майката"] diff --git a/test/cases/isolated_surrogate_marker.erl b/test/cases/isolated_surrogate_marker.erl new file mode 100644 index 0000000..06113c7 --- /dev/null +++ b/test/cases/isolated_surrogate_marker.erl @@ -0,0 +1 @@ +{error,{8,invalid_string}}. diff --git a/test/cases/isolated_surrogate_marker.json b/test/cases/isolated_surrogate_marker.json new file mode 100644 index 0000000..36959f4 --- /dev/null +++ b/test/cases/isolated_surrogate_marker.json @@ -0,0 +1 @@ +"\ud800" diff --git a/test/cases/leading_zero_in_number.erl b/test/cases/leading_zero_in_number.erl new file mode 100644 index 0000000..5bc5d8c --- /dev/null +++ b/test/cases/leading_zero_in_number.erl @@ -0,0 +1 @@ +{error,{17,invalid_json}}. diff --git a/test/cases/leading_zero_in_number.json b/test/cases/leading_zero_in_number.json new file mode 100644 index 0000000..959f5ba --- /dev/null +++ b/test/cases/leading_zero_in_number.json @@ -0,0 +1 @@ +{ "bad thing": 01 } diff --git a/test/cases/lonely_minus_sign.erl b/test/cases/lonely_minus_sign.erl new file mode 100644 index 0000000..5f172a4 --- /dev/null +++ b/test/cases/lonely_minus_sign.erl @@ -0,0 +1 @@ +{error,{83,invalid_number}}. diff --git a/test/cases/lonely_minus_sign.json b/test/cases/lonely_minus_sign.json new file mode 100644 index 0000000..85f69bd --- /dev/null +++ b/test/cases/lonely_minus_sign.json @@ -0,0 +1,7 @@ +[ + "foo", true, + true, "blue", + "baby where are you?", "oh boo hoo!", + - +] + diff --git a/test/cases/lonely_number.erl b/test/cases/lonely_number.erl new file mode 100644 index 0000000..b52af81 --- /dev/null +++ b/test/cases/lonely_number.erl @@ -0,0 +1 @@ +123456789. \ No newline at end of file diff --git a/test/cases/lonely_number.json b/test/cases/lonely_number.json new file mode 100644 index 0000000..e2e107a --- /dev/null +++ b/test/cases/lonely_number.json @@ -0,0 +1 @@ +123456789 \ No newline at end of file diff --git a/test/cases/map_close.erl b/test/cases/map_close.erl new file mode 100644 index 0000000..4aa294e --- /dev/null +++ b/test/cases/map_close.erl @@ -0,0 +1 @@ +{error,{1,invalid_json}}. diff --git a/test/cases/map_close.json b/test/cases/map_close.json new file mode 100644 index 0000000..5c34318 --- /dev/null +++ b/test/cases/map_close.json @@ -0,0 +1 @@ +} diff --git a/test/cases/map_open.erl b/test/cases/map_open.erl new file mode 100644 index 0000000..9fa573a --- /dev/null +++ b/test/cases/map_open.erl @@ -0,0 +1 @@ +{error,{3,truncated_json}}. diff --git a/test/cases/map_open.json b/test/cases/map_open.json new file mode 100644 index 0000000..98232c6 --- /dev/null +++ b/test/cases/map_open.json @@ -0,0 +1 @@ +{ diff --git a/test/cases/missing_integer_after_decimal_point.erl b/test/cases/missing_integer_after_decimal_point.erl new file mode 100644 index 0000000..046f3c7 --- /dev/null +++ b/test/cases/missing_integer_after_decimal_point.erl @@ -0,0 +1 @@ +{error,{4,invalid_number}}. diff --git a/test/cases/missing_integer_after_decimal_point.json b/test/cases/missing_integer_after_decimal_point.json new file mode 100644 index 0000000..2369f4b --- /dev/null +++ b/test/cases/missing_integer_after_decimal_point.json @@ -0,0 +1 @@ +10.e2 diff --git a/test/cases/missing_integer_after_exponent.erl b/test/cases/missing_integer_after_exponent.erl new file mode 100644 index 0000000..046f3c7 --- /dev/null +++ b/test/cases/missing_integer_after_exponent.erl @@ -0,0 +1 @@ +{error,{4,invalid_number}}. diff --git a/test/cases/missing_integer_after_exponent.json b/test/cases/missing_integer_after_exponent.json new file mode 100644 index 0000000..a62b45d --- /dev/null +++ b/test/cases/missing_integer_after_exponent.json @@ -0,0 +1 @@ +10e diff --git a/test/cases/non_utf8_char_in_string.erl b/test/cases/non_utf8_char_in_string.erl new file mode 100644 index 0000000..0ca5740 --- /dev/null +++ b/test/cases/non_utf8_char_in_string.erl @@ -0,0 +1 @@ +{error,{125,invalid_string}}. diff --git a/test/cases/non_utf8_char_in_string.json b/test/cases/non_utf8_char_in_string.json new file mode 100644 index 0000000..253a664 --- /dev/null +++ b/test/cases/non_utf8_char_in_string.json @@ -0,0 +1 @@ +{"CoreletAPIVersion":2,"CoreletType":"standalone","documentation":"A corelet that provides the capability to upload a folders contents into a users locker.","functions":[{"documentation":"Displays a dialog box that allows user to select a folder on the local system.","name":"ShowBrowseDialog","parameters":[{"documentation":"The callback function for results.","name":"callback","required":true,"type":"callback"}]},{"documentation":"Uploads all mp3 files in the folder provided.","name":"UploadFolder","parameters":[{"documentation":"The path to upload mp3 files from.","name":"path","required":true,"type":"string"},{"documentation":"The callback function for progress.","name":"callback","required":true,"type":"callback"}]},{"documentation":"Returns the server name to the current locker service.","name":"GetLockerService","parameters":[]},{"documentation":"Changes the name of the locker service.","name":"SetLockerService","parameters":[{"documentation":"The value of the locker service to set active.","name":"LockerService","required":true,"type":"string"}]},{"documentation":"Downloads locker files to the suggested folder.","name":"DownloadFile","parameters":[{"documentation":"The origin path of the locker file.","name":"path","required":true,"type":"string"},{"documentation":"The Window destination path of the locker file.","name":"destination","required":true,"type":"integer"},{"documentation":"The callback function for progress.","name":"callback","required":true,"type":"callback"}]}],"name":"LockerUploader","version":{"major":0,"micro":1,"minor":0},"versionString":"0.0.1"} \ No newline at end of file diff --git a/test/cases/null.erl b/test/cases/null.erl new file mode 100644 index 0000000..bed1002 --- /dev/null +++ b/test/cases/null.erl @@ -0,0 +1 @@ +null. \ No newline at end of file diff --git a/test/cases/null.json b/test/cases/null.json new file mode 100644 index 0000000..19765bd --- /dev/null +++ b/test/cases/null.json @@ -0,0 +1 @@ +null diff --git a/test/cases/null_then_garbage.erl b/test/cases/null_then_garbage.erl new file mode 100644 index 0000000..30b0113 --- /dev/null +++ b/test/cases/null_then_garbage.erl @@ -0,0 +1 @@ +{error,{5,invalid_trailing_data}}. diff --git a/test/cases/null_then_garbage.json b/test/cases/null_then_garbage.json new file mode 100644 index 0000000..7b65b35 --- /dev/null +++ b/test/cases/null_then_garbage.json @@ -0,0 +1 @@ +nullx diff --git a/test/cases/nulls_and_bools.erl b/test/cases/nulls_and_bools.erl new file mode 100644 index 0000000..68544ed --- /dev/null +++ b/test/cases/nulls_and_bools.erl @@ -0,0 +1,5 @@ +{[ + {<<"boolean, true">>, true}, + {<<"boolean, false">>, false}, + {<<"null">>, null} +]}. diff --git a/test/cases/nulls_and_bools.json b/test/cases/nulls_and_bools.json new file mode 100644 index 0000000..65eb01f --- /dev/null +++ b/test/cases/nulls_and_bools.json @@ -0,0 +1,5 @@ +{ + "boolean, true": true, + "boolean, false": false, + "null": null +} diff --git a/test/cases/simple.erl b/test/cases/simple.erl new file mode 100644 index 0000000..fdc4deb --- /dev/null +++ b/test/cases/simple.erl @@ -0,0 +1,5 @@ +{[ + {<<"this">>, <<"is">>}, + {<<"really">>, <<"simple">>}, + {<<"json">>, <<"right?">>} +]}. \ No newline at end of file diff --git a/test/cases/simple.json b/test/cases/simple.json new file mode 100644 index 0000000..9ed80c9 --- /dev/null +++ b/test/cases/simple.json @@ -0,0 +1,5 @@ +{ + "this": "is", + "really": "simple", + "json": "right?" +} diff --git a/test/cases/string_invalid_escape.erl b/test/cases/string_invalid_escape.erl new file mode 100644 index 0000000..eb20890 --- /dev/null +++ b/test/cases/string_invalid_escape.erl @@ -0,0 +1 @@ +{error,{63,invalid_string}}. diff --git a/test/cases/string_invalid_escape.json b/test/cases/string_invalid_escape.json new file mode 100644 index 0000000..54a38ac --- /dev/null +++ b/test/cases/string_invalid_escape.json @@ -0,0 +1 @@ +["\n foo \/ bar \r\f\\\ufffd\t\b\"\\ and you can't escape thi\s"] diff --git a/test/cases/string_invalid_hex_char.erl b/test/cases/string_invalid_hex_char.erl new file mode 100644 index 0000000..3fc65a4 --- /dev/null +++ b/test/cases/string_invalid_hex_char.erl @@ -0,0 +1 @@ +{error,{48,invalid_string}}. diff --git a/test/cases/string_invalid_hex_char.json b/test/cases/string_invalid_hex_char.json new file mode 100644 index 0000000..bde7ee9 --- /dev/null +++ b/test/cases/string_invalid_hex_char.json @@ -0,0 +1 @@ +"foo foo, blah blah \u0123 \u4567 \u89ab \uc/ef \uABCD \uEFFE bar baz bing" diff --git a/test/cases/string_with_escapes.erl b/test/cases/string_with_escapes.erl new file mode 100644 index 0000000..51f7f25 --- /dev/null +++ b/test/cases/string_with_escapes.erl @@ -0,0 +1,5 @@ +[ + <<"\n foo \/ bar \r\f\\", 239, 191, 186, "\t\b\"\\">>, + <<"\"and this string has an escape at the beginning">>, + <<"and this string has no escapes">> +]. diff --git a/test/cases/string_with_escapes.json b/test/cases/string_with_escapes.json new file mode 100644 index 0000000..532b5f4 --- /dev/null +++ b/test/cases/string_with_escapes.json @@ -0,0 +1,3 @@ +["\n foo \/ bar \r\f\\\ufffa\t\b\"\\", + "\"and this string has an escape at the beginning", + "and this string has no escapes" ] diff --git a/test/cases/string_with_invalid_newline.erl b/test/cases/string_with_invalid_newline.erl new file mode 100644 index 0000000..626a2f4 --- /dev/null +++ b/test/cases/string_with_invalid_newline.erl @@ -0,0 +1 @@ +{error,{67,invalid_string}}. diff --git a/test/cases/string_with_invalid_newline.json b/test/cases/string_with_invalid_newline.json new file mode 100644 index 0000000..2756f26 --- /dev/null +++ b/test/cases/string_with_invalid_newline.json @@ -0,0 +1,2 @@ +"la di dah. this is a string, and I can do this, \n, but not this +" \ No newline at end of file diff --git a/test/cases/three_byte_utf8.erl b/test/cases/three_byte_utf8.erl new file mode 100644 index 0000000..111676b --- /dev/null +++ b/test/cases/three_byte_utf8.erl @@ -0,0 +1,4 @@ +{[ + {<<"matzue">>, <<230, 157, 190, 230, 177, 159>>}, + {<<"asakusa">>, <<230, 181, 133, 232, 141, 137>>} +]}. \ No newline at end of file diff --git a/test/cases/three_byte_utf8.json b/test/cases/three_byte_utf8.json new file mode 100644 index 0000000..9c9e656 --- /dev/null +++ b/test/cases/three_byte_utf8.json @@ -0,0 +1 @@ +{"matzue": "松江", "asakusa": "浅草"} diff --git a/test/cases/true.erl b/test/cases/true.erl new file mode 100644 index 0000000..60c0d88 --- /dev/null +++ b/test/cases/true.erl @@ -0,0 +1 @@ +true. \ No newline at end of file diff --git a/test/cases/true.json b/test/cases/true.json new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/test/cases/true.json @@ -0,0 +1 @@ +true diff --git a/test/cases/true_then_garbage.erl b/test/cases/true_then_garbage.erl new file mode 100644 index 0000000..30b0113 --- /dev/null +++ b/test/cases/true_then_garbage.erl @@ -0,0 +1 @@ +{error,{5,invalid_trailing_data}}. diff --git a/test/cases/true_then_garbage.json b/test/cases/true_then_garbage.json new file mode 100644 index 0000000..9151612 --- /dev/null +++ b/test/cases/true_then_garbage.json @@ -0,0 +1 @@ +truex \ No newline at end of file diff --git a/test/cases/unescaped_bulgarian.erl b/test/cases/unescaped_bulgarian.erl new file mode 100644 index 0000000..2a31b61 --- /dev/null +++ b/test/cases/unescaped_bulgarian.erl @@ -0,0 +1,5 @@ +[ + <<208, 148, 208, 176, 32, 208, 156, 209, 131, 32, 208, 149, 208, + 177, 208, 176, 32, 208, 156, 208, 176, 208, 185, 208, 186, 208, + 176, 209, 130, 208, 176>> +]. diff --git a/test/cases/unescaped_bulgarian.json b/test/cases/unescaped_bulgarian.json new file mode 100644 index 0000000..f9a70a6 --- /dev/null +++ b/test/cases/unescaped_bulgarian.json @@ -0,0 +1 @@ +["Да Му Еба Майката"] diff --git a/test/etap.erl b/test/etap.erl new file mode 100644 index 0000000..82e0cfe --- /dev/null +++ b/test/etap.erl @@ -0,0 +1,612 @@ +%% Copyright (c) 2008-2009 Nick Gerakines +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines [http://socklabs.com/] +%% @author Jeremy Wall +%% @version 0.3.4 +%% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @todo Finish implementing the skip directive. +%% @todo Document the messages handled by this receive loop. +%% @todo Explain in documentation why we use a process to handle test input. +%% @doc etap is a TAP testing module for Erlang components and applications. +%% This module allows developers to test their software using the TAP method. +%% +%%

+%% TAP, the Test Anything Protocol, is a simple text-based interface between +%% testing modules in a test harness. TAP started life as part of the test +%% harness for Perl but now has implementations in C/C++, Python, PHP, Perl +%% and probably others by the time you read this. +%%

+%% +%% The testing process begins by defining a plan using etap:plan/1, running +%% a number of etap tests and then calling eta:end_tests/0. Please refer to +%% the Erlang modules in the t directory of this project for example tests. +-module(etap). +-vsn("0.3.4"). + +-export([ + ensure_test_server/0, + start_etap_server/0, + test_server/1, + msg/1, msg/2, + diag/1, diag/2, + expectation_mismatch_message/3, + plan/1, + end_tests/0, + not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, + fun_is/3, expect_fun/3, expect_fun/4, + is_greater/3, + skip/1, skip/2, + datetime/1, + skip/3, + bail/0, bail/1, + test_state/0, failure_count/0 +]). + +-export([ + contains_ok/3, + is_before/4 +]). + +-export([ + is_pid/2, + is_alive/2, + is_mfa/3 +]). + +-export([ + loaded_ok/2, + can_ok/2, can_ok/3, + has_attrib/2, is_attrib/3, + is_behaviour/2 +]). + +-export([ + dies_ok/2, + lives_ok/2, + throws_ok/3 +]). + + +-record(test_state, { + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" +}). + +%% @spec plan(N) -> Result +%% N = unknown | skip | {skip, string()} | integer() +%% Result = ok +%% @doc Create a test plan and boot strap the test server. +plan(unknown) -> + ensure_test_server(), + etap_server ! {self(), plan, unknown}, + ok; +plan(skip) -> + io:format("1..0 # skip~n"); +plan({skip, Reason}) -> + io:format("1..0 # skip ~s~n", [Reason]); +plan(N) when is_integer(N), N > 0 -> + ensure_test_server(), + etap_server ! {self(), plan, N}, + ok. + +%% @spec end_tests() -> ok +%% @doc End the current test plan and output test results. +%% @todo This should probably be done in the test_server process. +end_tests() -> + case whereis(etap_server) of + undefined -> self() ! true; + _ -> etap_server ! {self(), state} + end, + State = receive X -> X end, + if + State#test_state.planned == -1 -> + io:format("1..~p~n", [State#test_state.count]); + true -> + ok + end, + case whereis(etap_server) of + undefined -> ok; + _ -> etap_server ! done, ok + end. + +bail() -> + bail(""). + +bail(Reason) -> + etap_server ! {self(), diag, "Bail out! " ++ Reason}, + etap_server ! done, ok, + ok. + +%% @spec test_state() -> Return +%% Return = test_state_record() | {error, string()} +%% @doc Return the current test state +test_state() -> + etap_server ! {self(), state}, + receive + X when is_record(X, test_state) -> X + after + 1000 -> {error, "Timed out waiting for etap server reply.~n"} + end. + +%% @spec failure_count() -> Return +%% Return = integer() | {error, string()} +%% @doc Return the current failure count +failure_count() -> + case test_state() of + #test_state{fail=FailureCount} -> FailureCount; + X -> X + end. + +%% @spec msg(S) -> ok +%% S = string() +%% @doc Print a message in the test output. +msg(S) -> etap_server ! {self(), diag, S}, ok. + +%% @spec msg(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a message in the test output. +%% Function arguments are passed through io_lib:format/2. +msg(Format, Data) -> msg(io_lib:format(Format, Data)). + +%% @spec diag(S) -> ok +%% S = string() +%% @doc Print a debug/status message related to the test suite. +diag(S) -> msg("# " ++ S). + +%% @spec diag(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a debug/status message related to the test suite. +%% Function arguments are passed through io_lib:format/2. +diag(Format, Data) -> diag(io_lib:format(Format, Data)). + +%% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok +%% Got = any() +%% Expected = any() +%% Desc = string() +%% @doc Print an expectation mismatch message in the test output. +expectation_mismatch_message(Got, Expected, Desc) -> + msg(" ---"), + msg(" description: ~p", [Desc]), + msg(" found: ~p", [Got]), + msg(" wanted: ~p", [Expected]), + msg(" ..."), + ok. + +% @spec evaluate(Pass, Got, Expected, Desc) -> Result +%% Pass = true | false +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Evaluate a test statement, printing an expectation mismatch message +%% if the test failed. +evaluate(Pass, Got, Expected, Desc) -> + case mk_tap(Pass, Desc) of + false -> + expectation_mismatch_message(Got, Expected, Desc), + false; + true -> + true + end. + +%% @spec ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is true. +ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). + +%% @spec not_ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is false. +not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). + +%% @spec is_ok(Expr, Desc) -> Result +%% Expr = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). + +%% @spec is(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). + +%% @spec isnt(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are not the same. +isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). + +%% @spec is_greater(ValueA, ValueB, Desc) -> Result +%% ValueA = number() +%% ValueB = number() +%% Desc = string() +%% Result = true | false +%% @doc Assert that an integer is greater than another. +is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> + mk_tap(ValueA > ValueB, Desc). + +%% @spec any(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is in a list. +any(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), true, Desc); +any(Got, Items, Desc) -> + is(lists:member(Got, Items), true, Desc). + +%% @spec none(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is not in a list. +none(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), false, Desc); +none(Got, Items, Desc) -> + is(lists:member(Got, Items), false, Desc). + +%% @spec fun_is(Fun, Expected, Desc) -> Result +%% Fun = function() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match. +fun_is(Fun, Expected, Desc) when is_function(Fun) -> + is(Fun(Expected), true, Desc). + +%% @spec expect_fun(ExpectFun, Got, Desc) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc) -> + evaluate(ExpectFun(Got), Got, ExpectFun, Desc). + +%% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% ExpectStr = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc, ExpectStr) -> + evaluate(ExpectFun(Got), Got, ExpectStr, Desc). + +%% @equiv skip(TestFun, "") +skip(TestFun) when is_function(TestFun) -> + skip(TestFun, ""). + +%% @spec skip(TestFun, Reason) -> ok +%% TestFun = function() +%% Reason = string() +%% @doc Skip a test. +skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + begin_skip(Reason), + catch TestFun(), + end_skip(), + ok. + +%% @spec skip(Q, TestFun, Reason) -> ok +%% Q = true | false | function() +%% TestFun = function() +%% Reason = string() +%% @doc Skips a test conditionally. The first argument to this function can +%% either be the 'true' or 'false' atoms or a function that returns 'true' or +%% 'false'. +skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> + case QFun() of + true -> begin_skip(Reason), TestFun(), end_skip(); + _ -> TestFun() + end, + ok; + +skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> + begin_skip(Reason), + TestFun(), + end_skip(), + ok; + +skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + TestFun(), + ok. + +%% @private +begin_skip(Reason) -> + etap_server ! {self(), begin_skip, Reason}. + +%% @private +end_skip() -> + etap_server ! {self(), end_skip}. + +%% @spec contains_ok(string(), string(), string()) -> true | false +%% @doc Assert that a string is contained in another string. +contains_ok(Source, String, Desc) -> + etap:isnt( + string:str(Source, String), + 0, + Desc + ). + +%% @spec is_before(string(), string(), string(), string()) -> true | false +%% @doc Assert that a string comes before another string within a larger body. +is_before(Source, StringA, StringB, Desc) -> + etap:is_greater( + string:str(Source, StringB), + string:str(Source, StringA), + Desc + ). + +%% @doc Assert that a given variable is a pid. +is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); +is_pid(_, Desc) -> etap:ok(false, Desc). + +%% @doc Assert that a given process/pid is alive. +is_alive(Pid, Desc) -> + etap:ok(erlang:is_process_alive(Pid), Desc). + +%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. +is_mfa(Pid, MFA, Desc) -> + etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). + +%% @spec loaded_ok(atom(), string()) -> true | false +%% @doc Assert that a module has been loaded successfully. +loaded_ok(M, Desc) when is_atom(M) -> + etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). + +%% @spec can_ok(atom(), atom()) -> true | false +%% @doc Assert that a module exports a given function. +can_ok(M, F) when is_atom(M), is_atom(F) -> + Matches = [X || {X, _} <- M:module_info(exports), X == F], + etap:ok(Matches > 0, lists:concat([M, " can ", F])). + +%% @spec can_ok(atom(), atom(), integer()) -> true | false +%% @doc Assert that a module exports a given function with a given arity. +can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> + Matches = [X || X <- M:module_info(exports), X == {F, A}], + etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). + +%% @spec has_attrib(M, A) -> true | false +%% M = atom() +%% A = atom() +%% @doc Asserts that a module has a given attribute. +has_attrib(M, A) when is_atom(M), is_atom(A) -> + etap:isnt( + proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), + 'asdlkjasdlkads', + lists:concat([M, " has attribute ", A]) + ). + +%% @spec has_attrib(M, A. V) -> true | false +%% M = atom() +%% A = atom() +%% V = any() +%% @doc Asserts that a module has a given attribute with a given value. +is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> + etap:is( + proplists:get_value(A, M:module_info(attributes)), + [V], + lists:concat([M, "'s ", A, " is ", V]) + ). + +%% @spec is_behavior(M, B) -> true | false +%% M = atom() +%% B = atom() +%% @doc Asserts that a given module has a specific behavior. +is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> + is_attrib(M, behaviour, B). + +%% @doc Assert that an exception is raised when running a given function. +dies_ok(F, Desc) -> + case (catch F()) of + {'EXIT', _} -> etap:ok(true, Desc); + _ -> etap:ok(false, Desc) + end. + +%% @doc Assert that an exception is not raised when running a given function. +lives_ok(F, Desc) -> + etap:is(try_this(F), success, Desc). + +%% @doc Assert that the exception thrown by a function matches the given exception. +throws_ok(F, Exception, Desc) -> + try F() of + _ -> etap:ok(nok, Desc) + catch + _:E -> + etap:is(E, Exception, Desc) + end. + +%% @private +%% @doc Run a function and catch any exceptions. +try_this(F) when is_function(F, 0) -> + try F() of + _ -> success + catch + throw:E -> {throw, E}; + error:E -> {error, E}; + exit:E -> {exit, E} + end. + +%% @private +%% @doc Start the etap_server process if it is not running already. +ensure_test_server() -> + case whereis(etap_server) of + undefined -> + proc_lib:start(?MODULE, start_etap_server,[]); + _ -> + diag("The test server is already running.") + end. + +%% @private +%% @doc Start the etap_server loop and register itself as the etap_server +%% process. +start_etap_server() -> + catch register(etap_server, self()), + proc_lib:init_ack(ok), + etap:test_server(#test_state{ + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }). + + +%% @private +%% @doc The main etap_server receive/run loop. The etap_server receive loop +%% responds to seven messages apperatining to failure or passing of tests. +%% It is also used to initiate the testing process with the {_, plan, _} +%% message that clears the current test state. +test_server(State) -> + NewState = receive + {_From, plan, unknown} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + State#test_state{ + planned = -1, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, plan, N} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + io:format("1..~p~n", [N]), + State#test_state{ + planned = N, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, begin_skip, Reason} -> + State#test_state{ + skip = 1, + skip_reason = Reason + }; + {_From, end_skip} -> + State#test_state{ + skip = 0, + skip_reason = "" + }; + {_From, pass, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + pass = State#test_state.pass + 1 + }; + + {_From, fail, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + fail = State#test_state.fail + 1 + }; + {From, state} -> + From ! State, + State; + {_From, diag, Message} -> + io:format("~s~n", [Message]), + State; + {From, count} -> + From ! State#test_state.count, + State; + {From, is_skip} -> + From ! State#test_state.skip, + State; + done -> + exit(normal) + end, + test_server(NewState). + +%% @private +%% @doc Process the result of a test and send it to the etap_server process. +mk_tap(Result, Desc) -> + IsSkip = lib:sendw(etap_server, is_skip), + case [IsSkip, Result] of + [_, true] -> + etap_server ! {self(), pass, Desc}, + true; + [1, _] -> + etap_server ! {self(), pass, Desc}, + true; + _ -> + etap_server ! {self(), fail, Desc}, + false + end. + +%% @private +%% @doc Format a date/time string. +datetime(DateTime) -> + {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, + io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). + +%% @private +%% @doc Craft an output message taking skip/todo into consideration. +skip_diag(Message, 0, _) -> + Message; +skip_diag(_Message, 1, "") -> + " # SKIP"; +skip_diag(_Message, 1, Reason) -> + " # SKIP : " ++ Reason. diff --git a/test/util.erl b/test/util.erl new file mode 100644 index 0000000..fecc49b --- /dev/null +++ b/test/util.erl @@ -0,0 +1,31 @@ +-module(util). +-export([test_good/1, test_errors/1]). + +test_good(Cases) -> + lists:foreach(fun(Case) -> check_good(Case) end, Cases). + +test_errors(Cases) -> + lists:foreach(fun(Case) -> check_error(Case) end, Cases). + +ok_dec(J, _E) -> + lists:flatten(io_lib:format("Decoded ~p.", [J])). + +ok_enc(E, _J) -> + lists:flatten(io_lib:format("Encoded ~p", [E])). + +error_mesg(J) -> + lists:flatten(io_lib:format("Decoding ~p returns an error.", [J])). + +check_good({J, E}) -> + etap:is(jiffy:decode(J), {ok, E}, ok_dec(J, E)), + etap:is(jiffy:encode(E), {ok, J}, ok_enc(E, J)); +check_good({J, E, J2}) -> + etap:is(jiffy:decode(J), {ok, E}, ok_dec(J, E)), + etap:is(jiffy:encode(E), {ok, J2}, ok_enc(E, J2)). + +check_error(J) -> + etap:fun_is( + fun({error, _}) -> true; (_) -> false end, + jiffy:decode(J), + error_mesg(J) + ).