├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── c_src ├── alias.h ├── main.c ├── queue.c ├── queue.h ├── to_erl.c ├── to_js.c ├── util.c ├── util.h ├── vm.c └── vm.h ├── docs ├── emonk.graffle └── emonk.png ├── ebin └── emonk.app ├── rebar ├── rebar.config ├── src └── emonk.erl └── test ├── 001-basics.t ├── 003-test-ctx-creation.t ├── 004-basic-calls.t ├── 005-erl-to-js.t ├── 006-js-to-erl.t ├── 007-js-send-message.t ├── etap.erl ├── smoke ├── restarts.erl ├── segfault.es ├── separate-contexts.es └── single-context.es └── test_util.erl /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.o 3 | *.so 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2010 (c) Paul J. Davis 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | TEST_SUPPORT = \ 3 | test/etap.beam \ 4 | test/test_util.beam 5 | 6 | %.beam: %.erl 7 | erlc -o test/ $< 8 | 9 | all: 10 | ./rebar compile 11 | 12 | check: all $(TEST_SUPPORT) 13 | prove test/*.t 14 | 15 | clean: 16 | ./rebar clean 17 | rm -f test/*.beam 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | emonk - Accidentally Refactored erlang_js 2 | ========================================= 3 | 4 | [erlang_js][erlang_js] is awesome. But I started refactoring. Now there's emonk. 5 | 6 | With the latest versions of Emonk there's quite a bit of difference now. Emonk 7 | is NIF based and uses a thread-pool to move JavaScript execution off of the 8 | Erlang scheduler threads. Translation from Erlang terms to JavaScript objects 9 | uses a native translation step to avoid JSON overhead. I haven't gotten around 10 | to actually testing to see if there's any sort of appreciable difference in 11 | speed. 12 | 13 | [erlang_js]: http://hg.basho.com/erlang_js/ 14 | 15 | Requisites 16 | ---------- 17 | 18 | 1. A fairly recent version of Spidermonkey. I use the version from HomeBrew 19 | which uses [this url][spidermonkey]. 20 | 2. R14A. Emonk uses the new NIF API extensively. 21 | 22 | [spidermonkey]: http://hg.mozilla.org/tracemonkey/archive/57a6ad20eae9.tar.gz 23 | 24 | Building 25 | -------- 26 | 27 | Hopefully the build scenario is something like: 28 | 29 | $ git clone git://github.com/davisp/emonk.git 30 | $ cd emonk 31 | $ make 32 | $ make check 33 | 34 | Running 35 | ------- 36 | 37 | I've been using [etap][etap] to test as I code. Its awesome. You should use it. 38 | That said, running is pretty cake assuming emonk is on your Erlang code path: 39 | 40 | $ ERL_LIBS=~/awesome_projects/emonk/src erl -s emonk 41 | 1> {ok, Context} = emonk:create_ctx(). 42 | {ok, <<>>} % Note: The <<>> here is *not* an empty binary. Its a resource. 43 | 2> emonk:eval(Context, <<"var f = 2; f*3;">>). 44 | {ok, 6} 45 | 3> emonk:eval(Context, <<"var g = function(x) {return x*4;};">>). 46 | {ok, undefined} 47 | 4> emonk:call(Context, <<"g">>, [9]). 48 | {ok, 36} 49 | 50 | [etap]: http://github.com/ngerakines/etap 51 | 52 | Bugs and Things 53 | --------------- 54 | 55 | The best place to file [bugs][bugs] is at the [issue][bugs] tracker on 56 | [GitHub][github]. Less yapping, more tapping. Thanks [mattly][mattly]. 57 | 58 | [bugs]: http://github.com/davisp/emonk/issues 59 | [github]: http://github.com/davisp 60 | [mattly]: http://twitter.com/mattly 61 | 62 | Yep 63 | --- 64 | 65 | That's pretty much it. Mostly this was internal churn that I kinda started 66 | chasing around like my tail. Hopefully things are easy enough to follow in the 67 | logs and code. 68 | -------------------------------------------------------------------------------- /c_src/alias.h: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #ifndef EMONK_ALIAS_H 5 | #define EMONK_ALIAS_H 6 | 7 | typedef ERL_NIF_TERM ENTERM; 8 | typedef const ERL_NIF_TERM CENTERM; 9 | typedef ErlNifBinary ENBINARY; 10 | typedef ErlNifPid ENPID; 11 | 12 | #endif // Included alias.h 13 | -------------------------------------------------------------------------------- /c_src/main.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | 6 | #include "erl_nif.h" 7 | 8 | #include "alias.h" 9 | #include "util.h" 10 | #include "vm.h" 11 | 12 | #define GC_THRESHOLD 10485760 // 10 MiB 13 | #define MAX_BYTES 8388608 14 | #define MAX_MALLOC_BYTES 8388608 15 | #define MAX_WORKERS 64 16 | 17 | struct state_t 18 | { 19 | ErlNifMutex* lock; 20 | ErlNifResourceType* res_type; 21 | JSRuntime* runtime; 22 | int alive; 23 | }; 24 | 25 | typedef struct state_t* state_ptr; 26 | 27 | static int 28 | load(ErlNifEnv* env, void** priv, ENTERM load_info) 29 | { 30 | ErlNifResourceType* res; 31 | state_ptr state = (state_ptr) enif_alloc(sizeof(struct state_t)); 32 | const char* name = "Context"; 33 | int flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER; 34 | 35 | JS_SetCStringsAreUTF8(); 36 | 37 | if(state == NULL) goto error; 38 | 39 | state->lock = NULL; 40 | state->res_type = NULL; 41 | state->runtime = NULL; 42 | state->alive = 1; 43 | 44 | state->lock = enif_mutex_create("state_lock"); 45 | if(state->lock == NULL) goto error; 46 | 47 | res = enif_open_resource_type(env, NULL, name, vm_destroy, flags, NULL); 48 | if(res == NULL) goto error; 49 | state->res_type = res; 50 | 51 | state->runtime = JS_NewRuntime(GC_THRESHOLD); 52 | if(state->runtime == NULL) goto error; 53 | JS_SetGCParameter(state->runtime, JSGC_MAX_BYTES, MAX_BYTES); 54 | JS_SetGCParameter(state->runtime, JSGC_MAX_MALLOC_BYTES, MAX_MALLOC_BYTES); 55 | 56 | *priv = (void*) state; 57 | 58 | return 0; 59 | 60 | error: 61 | if(state != NULL) 62 | { 63 | if(state->lock != NULL) enif_mutex_destroy(state->lock); 64 | if(state->runtime != NULL) JS_DestroyRuntime(state->runtime); 65 | enif_free(state); 66 | } 67 | return -1; 68 | } 69 | 70 | static void 71 | unload(ErlNifEnv* env, void* priv) 72 | { 73 | state_ptr state = (state_ptr) priv; 74 | if(state->lock != NULL) enif_mutex_destroy(state->lock); 75 | if(state->runtime != NULL) JS_DestroyRuntime(state->runtime); 76 | enif_free(state); 77 | } 78 | 79 | static ENTERM 80 | create_ctx(ErlNifEnv* env, int argc, CENTERM argv[]) 81 | { 82 | state_ptr state = (state_ptr) enif_priv_data(env); 83 | unsigned int stack_size; 84 | vm_ptr vm; 85 | ENTERM ret; 86 | 87 | if(argc != 1 || !enif_get_uint(env, argv[0], &stack_size)) 88 | { 89 | return enif_make_badarg(env); 90 | } 91 | 92 | vm = vm_init(state->res_type, state->runtime, (size_t) stack_size); 93 | if(vm == NULL) return util_mk_error(env, "vm_init_failed"); 94 | 95 | ret = enif_make_resource(env, vm); 96 | enif_release_resource(vm); 97 | 98 | return util_mk_ok(env, ret); 99 | } 100 | 101 | static ENTERM 102 | eval(ErlNifEnv* env, int argc, CENTERM argv[]) 103 | { 104 | state_ptr state = (state_ptr) enif_priv_data(env); 105 | vm_ptr vm; 106 | ENPID pid; 107 | ENBINARY bin; 108 | 109 | if(argc != 4) return enif_make_badarg(env); 110 | 111 | if(!enif_get_resource(env, argv[0], state->res_type, (void**) &vm)) 112 | { 113 | return enif_make_badarg(env); 114 | } 115 | 116 | if(!enif_is_ref(env, argv[1])) 117 | { 118 | return util_mk_error(env, "invalid_ref"); 119 | } 120 | 121 | if(!enif_get_local_pid(env, argv[2], &pid)) 122 | { 123 | return util_mk_error(env, "invalid_pid"); 124 | } 125 | 126 | if(!enif_inspect_binary(env, argv[3], &bin)) 127 | { 128 | return util_mk_error(env, "invalid_script"); 129 | } 130 | 131 | if(!vm_add_eval(vm, argv[1], pid, bin)) 132 | { 133 | return util_mk_error(env, "error_creating_job"); 134 | } 135 | 136 | return util_mk_atom(env, "ok"); 137 | } 138 | 139 | static ENTERM 140 | call(ErlNifEnv* env, int argc, CENTERM argv[]) 141 | { 142 | state_ptr state = (state_ptr) enif_priv_data(env); 143 | vm_ptr vm; 144 | ENPID pid; 145 | 146 | if(argc != 5) return enif_make_badarg(env); 147 | 148 | if(!enif_get_resource(env, argv[0], state->res_type, (void**) &vm)) 149 | { 150 | return enif_make_badarg(env); 151 | } 152 | 153 | if(!enif_is_ref(env, argv[1])) 154 | { 155 | return util_mk_error(env, "invalid_ref"); 156 | } 157 | 158 | if(!enif_get_local_pid(env, argv[2], &pid)) 159 | { 160 | return util_mk_error(env, "invalid_pid"); 161 | } 162 | 163 | if(!enif_is_binary(env, argv[3])) 164 | { 165 | return util_mk_error(env, "invalid_name"); 166 | } 167 | 168 | if(!enif_is_list(env, argv[4])) 169 | { 170 | return util_mk_error(env, "invalid_args"); 171 | } 172 | 173 | if(!vm_add_call(vm, argv[1], pid, argv[3], argv[4])) 174 | { 175 | return util_mk_error(env, "error_creating_job"); 176 | } 177 | 178 | return util_mk_atom(env, "ok"); 179 | } 180 | 181 | static ENTERM 182 | send(ErlNifEnv* env, int argc, CENTERM argv[]) 183 | { 184 | state_ptr state = (state_ptr) enif_priv_data(env); 185 | vm_ptr vm; 186 | 187 | if(argc != 2) return enif_make_badarg(env); 188 | 189 | if(!enif_get_resource(env, argv[0], state->res_type, (void**) &vm)) 190 | { 191 | return enif_make_badarg(env); 192 | } 193 | 194 | if(!vm_send(vm, argv[1])) 195 | { 196 | return util_mk_error(env, "error_sending_response"); 197 | } 198 | 199 | return util_mk_atom(env, "ok"); 200 | } 201 | 202 | static ErlNifFunc nif_funcs[] = { 203 | {"create_ctx", 1, create_ctx}, 204 | {"eval", 4, eval}, 205 | {"call", 5, call}, 206 | {"send", 2, send} 207 | }; 208 | 209 | ERL_NIF_INIT(emonk, nif_funcs, &load, NULL, NULL, unload); 210 | 211 | -------------------------------------------------------------------------------- /c_src/queue.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | #include 6 | 7 | #include "queue.h" 8 | 9 | struct qitem_t 10 | { 11 | struct qitem_t* next; 12 | void* data; 13 | }; 14 | 15 | typedef struct qitem_t* qitem_ptr; 16 | 17 | struct queue_t 18 | { 19 | ErlNifMutex* lock; 20 | ErlNifCond* cond; 21 | qitem_ptr head; 22 | qitem_ptr tail; 23 | void* message; 24 | int length; 25 | }; 26 | 27 | queue_ptr 28 | queue_create(const char* name) 29 | { 30 | queue_ptr ret; 31 | 32 | ret = (queue_ptr) enif_alloc(sizeof(struct queue_t)); 33 | if(ret == NULL) goto error; 34 | 35 | ret->lock = NULL; 36 | ret->cond = NULL; 37 | ret->head = NULL; 38 | ret->tail = NULL; 39 | ret->message = NULL; 40 | ret->length = 0; 41 | 42 | ret->lock = enif_mutex_create("queue_lock"); 43 | if(ret->lock == NULL) goto error; 44 | 45 | ret->cond = enif_cond_create("queue_cond"); 46 | if(ret->cond == NULL) goto error; 47 | 48 | return ret; 49 | 50 | error: 51 | if(ret->lock != NULL) enif_mutex_destroy(ret->lock); 52 | if(ret->cond != NULL) enif_cond_destroy(ret->cond); 53 | if(ret != NULL) enif_free(ret); 54 | return NULL; 55 | } 56 | 57 | void 58 | queue_destroy(queue_ptr queue) 59 | { 60 | ErlNifMutex* lock; 61 | ErlNifCond* cond; 62 | int length; 63 | 64 | enif_mutex_lock(queue->lock); 65 | lock = queue->lock; 66 | cond = queue->cond; 67 | length = queue->length; 68 | 69 | queue->lock = NULL; 70 | queue->cond = NULL; 71 | queue->head = NULL; 72 | queue->tail = NULL; 73 | queue->length = -1; 74 | enif_mutex_unlock(lock); 75 | 76 | assert(length == 0 && "Attempting to destroy a non-empty queue."); 77 | enif_cond_destroy(cond); 78 | enif_mutex_destroy(lock); 79 | enif_free(queue); 80 | } 81 | 82 | int 83 | queue_has_item(queue_ptr queue) 84 | { 85 | int ret; 86 | 87 | enif_mutex_lock(queue->lock); 88 | ret = (queue->head != NULL); 89 | enif_mutex_unlock(queue->lock); 90 | 91 | return ret; 92 | } 93 | 94 | int 95 | queue_push(queue_ptr queue, void* item) 96 | { 97 | qitem_ptr entry = (qitem_ptr) enif_alloc(sizeof(struct qitem_t)); 98 | if(entry == NULL) return 0; 99 | 100 | entry->data = item; 101 | entry->next = NULL; 102 | 103 | enif_mutex_lock(queue->lock); 104 | 105 | assert(queue->length >= 0 && "Invalid queue size at push"); 106 | 107 | if(queue->tail != NULL) 108 | { 109 | queue->tail->next = entry; 110 | } 111 | 112 | queue->tail = entry; 113 | 114 | if(queue->head == NULL) 115 | { 116 | queue->head = queue->tail; 117 | } 118 | 119 | queue->length += 1; 120 | 121 | enif_cond_signal(queue->cond); 122 | enif_mutex_unlock(queue->lock); 123 | 124 | return 1; 125 | } 126 | 127 | void* 128 | queue_pop(queue_ptr queue) 129 | { 130 | qitem_ptr entry; 131 | void* item; 132 | 133 | enif_mutex_lock(queue->lock); 134 | 135 | // Wait for an item to become available. 136 | while(queue->head == NULL) 137 | { 138 | enif_cond_wait(queue->cond, queue->lock); 139 | } 140 | 141 | assert(queue->length >= 0 && "Invalid queue size at pop."); 142 | 143 | // Woke up because queue->head != NULL 144 | // Remove the entry and return the payload. 145 | 146 | entry = queue->head; 147 | queue->head = entry->next; 148 | entry->next = NULL; 149 | 150 | if(queue->head == NULL) 151 | { 152 | assert(queue->tail == entry && "Invalid queue state: Bad tail."); 153 | queue->tail = NULL; 154 | } 155 | 156 | queue->length -= 1; 157 | 158 | enif_mutex_unlock(queue->lock); 159 | 160 | item = entry->data; 161 | enif_free(entry); 162 | 163 | return item; 164 | } 165 | 166 | int 167 | queue_send(queue_ptr queue, void* item) 168 | { 169 | enif_mutex_lock(queue->lock); 170 | assert(queue->message == NULL && "Attempting to send multiple messages."); 171 | queue->message = item; 172 | enif_cond_signal(queue->cond); 173 | enif_mutex_unlock(queue->lock); 174 | return 1; 175 | } 176 | 177 | void* 178 | queue_receive(queue_ptr queue) 179 | { 180 | void* item; 181 | 182 | enif_mutex_lock(queue->lock); 183 | 184 | // Wait for an item to become available. 185 | while(queue->message == NULL) 186 | { 187 | enif_cond_wait(queue->cond, queue->lock); 188 | } 189 | 190 | item = queue->message; 191 | queue->message = NULL; 192 | 193 | enif_mutex_unlock(queue->lock); 194 | 195 | return item; 196 | } -------------------------------------------------------------------------------- /c_src/queue.h: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #ifndef EMONK_QUEUE_H 5 | #define EMONK_QUEUE_H 6 | 7 | #include "erl_nif.h" 8 | 9 | typedef struct queue_t* queue_ptr; 10 | 11 | queue_ptr queue_create(); 12 | void queue_destroy(queue_ptr queue); 13 | 14 | int queue_has_item(queue_ptr queue); 15 | 16 | int queue_push(queue_ptr queue, void* item); 17 | void* queue_pop(queue_ptr queue); 18 | 19 | int queue_send(queue_ptr queue, void* item); 20 | void* queue_receive(queue_ptr); 21 | 22 | #endif // Included queue.h 23 | -------------------------------------------------------------------------------- /c_src/to_erl.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | 6 | #include "util.h" 7 | 8 | #define OK 1 9 | #define ERROR 0 10 | 11 | int to_erl_intern(ErlNifEnv* env, JSContext* cx, jsval val, ERL_NIF_TERM* term); 12 | 13 | int 14 | to_erl_atom(ErlNifEnv* env, const char* atom, ERL_NIF_TERM* term) 15 | { 16 | *term = util_mk_atom(env, atom); 17 | return OK; 18 | } 19 | 20 | int 21 | to_erl_string(ErlNifEnv* env, JSContext* cx, jsval val, ERL_NIF_TERM* term) 22 | { 23 | JSString *str; 24 | ErlNifBinary bin; 25 | const char* data; 26 | size_t len; 27 | 28 | str = JS_ValueToString(cx, val); 29 | data = JS_GetStringBytesZ(cx, str); 30 | if(data == NULL) return ERROR; 31 | len = strlen(data); 32 | 33 | if(!enif_alloc_binary(len, &bin)) 34 | { 35 | return ERROR; 36 | } 37 | 38 | memcpy(bin.data, data, len); 39 | *term = enif_make_binary(env, &bin); 40 | return OK; 41 | } 42 | 43 | int 44 | to_erl_int(ErlNifEnv* env, JSContext* cx, jsval val, ERL_NIF_TERM* term) 45 | { 46 | int32_t rval; 47 | if(!JS_ValueToInt32(cx, val, &rval)) return ERROR; 48 | *term = enif_make_int(env, rval); 49 | return OK; 50 | } 51 | 52 | int 53 | to_erl_float(ErlNifEnv* env, JSContext* cx, jsval val, ERL_NIF_TERM* term) 54 | { 55 | double rval; 56 | if(!JS_ValueToNumber(cx, val, &rval)) return ERROR; 57 | *term = enif_make_double(env, rval); 58 | return OK; 59 | } 60 | 61 | int 62 | to_erl_array(ErlNifEnv* env, JSContext* cx, JSObject* obj, ERL_NIF_TERM* term) 63 | { 64 | ERL_NIF_TERM* array = NULL; 65 | int ret = ERROR; 66 | unsigned int length; 67 | jsval v; 68 | int i; 69 | 70 | if(!JS_GetArrayLength(cx, obj, &length)) return ERROR; 71 | 72 | array = (ERL_NIF_TERM*) enif_alloc(length * sizeof(ERL_NIF_TERM)); 73 | if(array == NULL) goto done; 74 | 75 | for(i = 0; i < length; i++) 76 | { 77 | if(!JS_GetElement(cx, obj, i, &v)) goto done; 78 | if(!to_erl_intern(env, cx, v, array+i)) goto done; 79 | } 80 | 81 | *term = enif_make_list_from_array(env, array, length); 82 | ret = OK; 83 | 84 | done: 85 | if(array != NULL) enif_free(array); 86 | return ret; 87 | } 88 | 89 | int 90 | to_erl_object(ErlNifEnv* env, JSContext* cx, JSObject* obj, ERL_NIF_TERM* term) 91 | { 92 | ERL_NIF_TERM* array = NULL; 93 | ERL_NIF_TERM list; 94 | ERL_NIF_TERM keyterm; 95 | ERL_NIF_TERM valterm; 96 | JSObject* iter; 97 | jsid idp; 98 | jsval val; 99 | int length; 100 | int index; 101 | int ret = ERROR; 102 | 103 | iter = JS_NewPropertyIterator(cx, obj); 104 | if(iter == NULL) goto done; 105 | 106 | length = 0; 107 | while(JS_NextProperty(cx, iter, &idp)) 108 | { 109 | if(idp == JSVAL_VOID) break; 110 | length += 1; 111 | } 112 | 113 | array = enif_alloc(length * sizeof(ERL_NIF_TERM)); 114 | if(array == NULL) goto done; 115 | 116 | iter = JS_NewPropertyIterator(cx, obj); 117 | if(iter == NULL) goto done; 118 | 119 | index = 0; 120 | while(JS_NextProperty(cx, iter, &idp)) 121 | { 122 | if(idp == JSVAL_VOID) 123 | { 124 | list = enif_make_list_from_array(env, array, length); 125 | *term = enif_make_tuple1(env, list); 126 | ret = OK; 127 | goto done; 128 | } 129 | 130 | if(!JS_IdToValue(cx, idp, &val)) goto done; 131 | if(!to_erl_string(env, cx, val, &keyterm)) goto done; 132 | if(!JS_GetPropertyById(cx, obj, idp, &val)) goto done; 133 | if(!to_erl_intern(env, cx, val, &valterm)) goto done; 134 | 135 | array[index] = enif_make_tuple2(env, keyterm, valterm); 136 | index += 1; 137 | } 138 | 139 | done: 140 | if(array != NULL) enif_free(array); 141 | return ret; 142 | } 143 | 144 | int 145 | to_erl_convert(ErlNifEnv* env, JSContext* cx, JSObject* obj, ERL_NIF_TERM* term) 146 | { 147 | JSObject* func; 148 | jsval tojson; 149 | jsval rval; 150 | 151 | if(!JS_GetProperty(cx, obj, "toJSON", &tojson)) 152 | { 153 | return ERROR; 154 | } 155 | 156 | if(!JSVAL_IS_OBJECT(tojson)) return ERROR; 157 | func = JSVAL_TO_OBJECT(tojson); 158 | if(func == NULL) return ERROR; 159 | if(!JS_ObjectIsFunction(cx, func)) return ERROR; 160 | 161 | if(!JS_CallFunctionValue(cx, obj, tojson, 0, NULL, &rval)) 162 | { 163 | return ERROR; 164 | } 165 | 166 | return to_erl_intern(env, cx, rval, term); 167 | } 168 | 169 | int 170 | to_erl_intern(ErlNifEnv* env, JSContext* cx, jsval val, ERL_NIF_TERM* term) 171 | { 172 | JSObject* obj = NULL; 173 | JSType type = JS_TypeOfValue(cx, val); 174 | 175 | if(val == JSVAL_NULL) 176 | { 177 | return to_erl_atom(env, "null", term); 178 | } 179 | else if(val == JSVAL_VOID) 180 | { 181 | return ERROR; 182 | } 183 | else if(type == JSTYPE_BOOLEAN) 184 | { 185 | if(val == JSVAL_TRUE) 186 | return to_erl_atom(env, "true", term); 187 | else 188 | return to_erl_atom(env, "false", term); 189 | } 190 | else if(type == JSTYPE_STRING) 191 | { 192 | return to_erl_string(env, cx, val, term); 193 | } 194 | else if(type == JSTYPE_XML) 195 | { 196 | return to_erl_string(env, cx, val, term); 197 | } 198 | else if(type == JSTYPE_NUMBER) 199 | { 200 | if(JSVAL_IS_INT(val)) 201 | return to_erl_int(env, cx, val, term); 202 | else 203 | return to_erl_float(env, cx, val, term); 204 | } 205 | else if(type == JSTYPE_OBJECT) 206 | { 207 | obj = JSVAL_TO_OBJECT(val); 208 | 209 | if(OK == to_erl_convert(env, cx, obj, term)) 210 | { 211 | return OK; 212 | } 213 | 214 | if(JS_IsArrayObject(cx, obj)) 215 | { 216 | return to_erl_array(env, cx, obj, term); 217 | } 218 | 219 | return to_erl_object(env, cx, obj, term); 220 | } 221 | 222 | return ERROR; 223 | } 224 | 225 | ERL_NIF_TERM 226 | to_erl(ErlNifEnv* env, JSContext* cx, jsval val) 227 | { 228 | ERL_NIF_TERM ret = util_mk_atom(env, "undefined"); 229 | 230 | if(!to_erl_intern(env, cx, val, &ret)) 231 | { 232 | return util_mk_atom(env, "undefined"); 233 | } 234 | 235 | return ret; 236 | } 237 | -------------------------------------------------------------------------------- /c_src/to_js.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | 6 | #include "util.h" 7 | 8 | jsval 9 | to_js_special(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM term) 10 | { 11 | JSString* str = NULL; 12 | char atom[512]; // Pretty sure there's a 256 byte limit 13 | 14 | if(!enif_get_atom(env, term, atom, 512, ERL_NIF_LATIN1)) 15 | { 16 | return JSVAL_VOID; 17 | } 18 | else if(strcmp(atom, "true") == 0) 19 | { 20 | return JSVAL_TRUE; 21 | } 22 | else if(strcmp(atom, "false") == 0) 23 | { 24 | return JSVAL_FALSE; 25 | } 26 | else if(strcmp(atom, "null") == 0) 27 | { 28 | return JSVAL_NULL; 29 | } 30 | else 31 | { 32 | str = JS_NewStringCopyZ(cx, atom); 33 | if(str == NULL) return JSVAL_VOID; 34 | return STRING_TO_JSVAL(str); 35 | } 36 | } 37 | 38 | jsval 39 | to_js_number(JSContext* cx, double value) 40 | { 41 | jsval ret; 42 | 43 | if(!JS_NewNumberValue(cx, value, &ret)) 44 | { 45 | return JSVAL_VOID; 46 | } 47 | 48 | return ret; 49 | } 50 | 51 | jsval 52 | to_js_string(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM term) 53 | { 54 | ErlNifBinary bin; 55 | JSString* str; 56 | jschar* chars; 57 | size_t charslen; 58 | 59 | if(!enif_inspect_binary(env, term, &bin)) 60 | { 61 | return JSVAL_VOID; 62 | } 63 | 64 | if(!JS_DecodeBytes(cx, (char*) bin.data, bin.size, NULL, &charslen)) 65 | { 66 | return JSVAL_VOID; 67 | } 68 | 69 | chars = JS_malloc(cx, (charslen + 1) * sizeof(jschar)); 70 | if(chars == NULL) return JSVAL_VOID; 71 | 72 | if(!JS_DecodeBytes(cx, (char*) bin.data, bin.size, chars, &charslen)) 73 | { 74 | JS_free(cx, chars); 75 | return JSVAL_VOID; 76 | } 77 | chars[charslen] = '\0'; 78 | 79 | str = JS_NewUCString(cx, chars, charslen); 80 | if(!str) 81 | { 82 | JS_free(cx, chars); 83 | return JSVAL_VOID; 84 | } 85 | 86 | return STRING_TO_JSVAL(str); 87 | } 88 | 89 | jsval 90 | to_js_empty_array(JSContext* cx) 91 | { 92 | JSObject* ret = JS_NewArrayObject(cx, 0, NULL); 93 | if(ret == NULL) return JSVAL_VOID; 94 | return OBJECT_TO_JSVAL(ret); 95 | } 96 | 97 | jsval 98 | to_js_array(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM head, ERL_NIF_TERM tail) 99 | { 100 | JSObject* ret; 101 | jsval val; 102 | jsint i; 103 | 104 | ret = JS_NewArrayObject(cx, 0, NULL); 105 | if(ret == NULL) return JSVAL_VOID; 106 | 107 | i = 0; 108 | do { 109 | val = to_js(env, cx, head); 110 | if(val == JSVAL_VOID) return JSVAL_VOID; 111 | if(!JS_SetElement(cx, ret, i, &val)) return JSVAL_VOID; 112 | i += 1; 113 | } while(enif_get_list_cell(env, tail, &head, &tail)); 114 | 115 | return OBJECT_TO_JSVAL(ret); 116 | } 117 | 118 | jsval 119 | to_js_key(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM term) 120 | { 121 | if(enif_is_atom(env, term)) 122 | { 123 | return to_js_special(env, cx, term); 124 | } 125 | else if(enif_is_binary(env, term)) 126 | { 127 | return to_js_string(env, cx, term); 128 | } 129 | else 130 | { 131 | return JSVAL_VOID; 132 | } 133 | } 134 | 135 | jsval 136 | to_js_object(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM list) 137 | { 138 | JSObject* ret; 139 | jsval kval; 140 | jsval vval; 141 | jsid idp; 142 | ERL_NIF_TERM head; 143 | ERL_NIF_TERM tail; 144 | const ERL_NIF_TERM* pair; 145 | int arity; 146 | 147 | ret = JS_NewObject(cx, NULL, NULL, NULL); 148 | if(ret == NULL) return JSVAL_VOID; 149 | 150 | if(enif_is_empty_list(env, list)) 151 | { 152 | return OBJECT_TO_JSVAL(ret); 153 | } 154 | 155 | if(!enif_get_list_cell(env, list, &head, &tail)) 156 | { 157 | return JSVAL_VOID; 158 | } 159 | 160 | do { 161 | if(!enif_get_tuple(env, head, &arity, &pair)) 162 | { 163 | return JSVAL_VOID; 164 | } 165 | 166 | if(arity != 2) 167 | { 168 | return JSVAL_VOID; 169 | } 170 | 171 | kval = to_js_key(env, cx, pair[0]); 172 | if(kval == JSVAL_VOID) return JSVAL_VOID; 173 | if(!JS_ValueToId(cx, kval, &idp)) return JSVAL_VOID; 174 | vval = to_js(env, cx, pair[1]); 175 | if(vval == JSVAL_VOID) return JSVAL_VOID; 176 | 177 | if(!JS_SetPropertyById(cx, ret, idp, &vval)) 178 | { 179 | return JSVAL_VOID; 180 | } 181 | } while(enif_get_list_cell(env, tail, &head, &tail)); 182 | 183 | return OBJECT_TO_JSVAL(ret); 184 | } 185 | 186 | jsval 187 | to_js(ErlNifEnv* env, JSContext* cx, ERL_NIF_TERM term) 188 | { 189 | int intval; 190 | unsigned int uintval; 191 | long longval; 192 | unsigned long ulongval; 193 | double doubleval; 194 | ERL_NIF_TERM head; 195 | ERL_NIF_TERM tail; 196 | const ERL_NIF_TERM* tuple; 197 | int arity; 198 | 199 | if(enif_is_atom(env, term)) 200 | { 201 | return to_js_special(env, cx, term); 202 | } 203 | 204 | if(enif_is_binary(env, term)) 205 | { 206 | return to_js_string(env, cx, term); 207 | } 208 | 209 | if(enif_is_empty_list(env, term)) 210 | { 211 | return to_js_empty_array(cx); 212 | } 213 | 214 | if(enif_get_int(env, term, &intval)) 215 | { 216 | return to_js_number(cx, (double) intval); 217 | } 218 | 219 | if(enif_get_uint(env, term, &uintval)) 220 | { 221 | return to_js_number(cx, (double) uintval); 222 | } 223 | 224 | if(enif_get_long(env, term, &longval)) 225 | { 226 | return to_js_number(cx, (double) longval); 227 | } 228 | 229 | if(enif_get_ulong(env, term, &ulongval)) 230 | { 231 | return to_js_number(cx, (double) ulongval); 232 | } 233 | 234 | if(enif_get_double(env, term, &doubleval)) 235 | { 236 | return to_js_number(cx, doubleval); 237 | } 238 | 239 | if(enif_get_list_cell(env, term, &head, &tail)) 240 | { 241 | return to_js_array(env, cx, head, tail); 242 | } 243 | 244 | if(enif_get_tuple(env, term, &arity, &tuple)) 245 | { 246 | if(arity == 1) 247 | { 248 | return to_js_object(env, cx, tuple[0]); 249 | } 250 | } 251 | 252 | return JSVAL_VOID; 253 | } 254 | -------------------------------------------------------------------------------- /c_src/util.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | 6 | #include "util.h" 7 | 8 | ENTERM 9 | util_mk_atom(ErlNifEnv* env, const char* atom) 10 | { 11 | ENTERM ret; 12 | 13 | if(enif_make_existing_atom(env, atom, &ret, ERL_NIF_LATIN1)) return ret; 14 | 15 | return enif_make_atom(env, atom); 16 | } 17 | 18 | ENTERM 19 | util_mk_ok(ErlNifEnv* env, ENTERM value) 20 | { 21 | ENTERM ok = util_mk_atom(env, "ok"); 22 | return enif_make_tuple2(env, ok, value); 23 | } 24 | 25 | ENTERM 26 | util_mk_error(ErlNifEnv* env, const char* reason) 27 | { 28 | ENTERM error = util_mk_atom(env, "error"); 29 | return enif_make_tuple2(env, error, util_mk_atom(env, reason)); 30 | } 31 | 32 | void 33 | util_debug_jsval(JSContext* cx, jsval val) 34 | { 35 | JSString* str; 36 | char* bytes; 37 | 38 | str = JS_ValueToString(cx, val); 39 | if(!str) 40 | { 41 | fprintf(stderr, "DEBUG: Unable to convert value.\n"); 42 | return; 43 | } 44 | 45 | bytes = JS_EncodeString(cx, str); 46 | if(!bytes) 47 | { 48 | fprintf(stderr, "DEBUG: Unable to encode string.\n"); 49 | return; 50 | } 51 | 52 | fprintf(stderr, "%s\n", bytes); 53 | } 54 | -------------------------------------------------------------------------------- /c_src/util.h: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #ifndef EMONK_UTIL_H 5 | #define EMONK_UTIL_H 6 | 7 | #include 8 | #include 9 | 10 | #include "alias.h" 11 | 12 | jsval to_js(ErlNifEnv* env, JSContext* cx, ENTERM term); 13 | ENTERM to_erl(ErlNifEnv* env, JSContext* cx, jsval val); 14 | 15 | ENTERM util_mk_atom(ErlNifEnv* env, const char* atom); 16 | ENTERM util_mk_ok(ErlNifEnv* env, ENTERM value); 17 | ENTERM util_mk_error(ErlNifEnv* env, const char* reason); 18 | 19 | void util_debug_jsval(JSContext* cx, jsval val); 20 | 21 | #endif // Included util.h 22 | -------------------------------------------------------------------------------- /c_src/vm.c: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #include 5 | #include 6 | 7 | #include "queue.h" 8 | #include "util.h" 9 | #include "vm.h" 10 | 11 | typedef enum 12 | { 13 | job_unknown, 14 | job_close, 15 | job_eval, 16 | job_call, 17 | job_response 18 | } job_type_e; 19 | 20 | struct job_t 21 | { 22 | job_type_e type; 23 | 24 | ErlNifEnv* env; 25 | ENTERM ref; 26 | ErlNifPid pid; 27 | 28 | ErlNifBinary script; 29 | ENTERM name; 30 | ENTERM args; 31 | 32 | ENTERM error; 33 | }; 34 | 35 | typedef struct job_t* job_ptr; 36 | 37 | struct vm_t 38 | { 39 | ErlNifTid tid; 40 | ErlNifThreadOpts* opts; 41 | JSRuntime* runtime; 42 | queue_ptr jobs; 43 | job_ptr curr_job; 44 | size_t stack_size; 45 | int alive; 46 | }; 47 | 48 | static JSClass global_class = { 49 | "global", 50 | JSCLASS_GLOBAL_FLAGS, 51 | JS_PropertyStub, 52 | JS_PropertyStub, 53 | JS_PropertyStub, 54 | JS_PropertyStub, 55 | JS_EnumerateStub, 56 | JS_ResolveStub, 57 | JS_ConvertStub, 58 | JS_FinalizeStub, 59 | JSCLASS_NO_OPTIONAL_MEMBERS 60 | }; 61 | 62 | void* vm_run(void* arg); 63 | ENTERM vm_eval(JSContext* cx, JSObject* gl, job_ptr job); 64 | ENTERM vm_call(JSContext* cx, JSObject* gl, job_ptr job); 65 | void vm_report_error(JSContext* cx, const char* mesg, JSErrorReport* report); 66 | ENTERM vm_mk_ok(ErlNifEnv* env, ENTERM reason); 67 | ENTERM vm_mk_error(ErlNifEnv* env, ENTERM reason); 68 | ENTERM vm_mk_message(ErlNifEnv* env, ENTERM data); 69 | 70 | // Job constructor and destructor 71 | 72 | job_ptr 73 | job_create() 74 | { 75 | job_ptr ret = (job_ptr) enif_alloc(sizeof(struct job_t)); 76 | if(ret == NULL) return NULL; 77 | 78 | ret->type = job_unknown; 79 | ret->env = enif_alloc_env(); 80 | if(ret->env == NULL) goto error; 81 | 82 | ret->ref = 0; 83 | ret->script.data = NULL; 84 | ret->script.size = 0; 85 | ret->error = 0; 86 | 87 | return ret; 88 | 89 | error: 90 | if(ret->env != NULL) enif_free_env(ret->env); 91 | enif_free(ret); 92 | return NULL; 93 | } 94 | 95 | void 96 | job_destroy(void* obj) 97 | { 98 | job_ptr job = (job_ptr) obj; 99 | if(job->script.data != NULL) enif_release_binary(&job->script); 100 | if(job->env != NULL) enif_free_env(job->env); 101 | enif_free(job); 102 | } 103 | 104 | // For the erlang global object. 105 | 106 | static JSClass jserl_class = { 107 | "JSErlang", 108 | 0, 109 | JS_PropertyStub, 110 | JS_PropertyStub, 111 | JS_PropertyStub, 112 | JS_PropertyStub, 113 | JS_EnumerateStub, 114 | JS_ResolveStub, 115 | JS_ConvertStub, 116 | JS_FinalizeStub, 117 | JSCLASS_NO_OPTIONAL_MEMBERS 118 | }; 119 | 120 | static JSBool 121 | jserl_send(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval) 122 | { 123 | vm_ptr vm = (vm_ptr) JS_GetContextPrivate(cx); 124 | ErlNifEnv* env; 125 | job_ptr job; 126 | ENTERM mesg; 127 | 128 | if(argc < 0) 129 | { 130 | return JS_FALSE; 131 | } 132 | 133 | assert(vm != NULL && "Context has no vm."); 134 | 135 | env = enif_alloc_env(); 136 | mesg = vm_mk_message(env, to_erl(env, cx, argv[0])); 137 | 138 | // If pid is not alive, raise an error. 139 | // XXX: Can I make this uncatchable? 140 | if(!enif_send(NULL, &(vm->curr_job->pid), env, mesg)) 141 | { 142 | JS_ReportError(cx, "Context closing."); 143 | return JS_FALSE; 144 | } 145 | 146 | job = queue_receive(vm->jobs); 147 | if(job->type == job_close) 148 | { 149 | // XXX: Can I make this uncatchable? 150 | job_destroy(job); 151 | JS_ReportError(cx, "Context closing."); 152 | return JS_FALSE; 153 | } 154 | 155 | assert(job->type == job_response && "Invalid message response."); 156 | 157 | *rval = to_js(job->env, cx, job->args); 158 | job_destroy(job); 159 | 160 | return JS_TRUE; 161 | } 162 | 163 | int 164 | install_jserl(JSContext* cx, JSObject* gl) 165 | { 166 | JSObject* obj; 167 | 168 | obj = JS_NewObject(cx, &jserl_class, NULL, NULL); 169 | if(obj == NULL) 170 | { 171 | return 0; 172 | } 173 | 174 | if(!JS_DefineFunction(cx, obj, "send", jserl_send, 1, 175 | JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) 176 | { 177 | return 0; 178 | } 179 | 180 | if(!JS_DefineProperty(cx, gl, "erlang", OBJECT_TO_JSVAL(obj), NULL, NULL, 181 | JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) 182 | { 183 | return 0; 184 | } 185 | 186 | return 1; 187 | } 188 | 189 | // 190 | // VM Related API 191 | // 192 | 193 | vm_ptr 194 | vm_init(ErlNifResourceType* res_type, JSRuntime* runtime, size_t stack_size) 195 | { 196 | vm_ptr vm = (vm_ptr) enif_alloc_resource(res_type, sizeof(struct vm_t)); 197 | if(vm == NULL) return NULL; 198 | 199 | vm->runtime = runtime; 200 | vm->curr_job = NULL; 201 | vm->stack_size = stack_size; 202 | 203 | vm->jobs = queue_create(); 204 | if(vm->jobs == NULL) goto error; 205 | 206 | vm->opts = enif_thread_opts_create("vm_thread_opts"); 207 | if(enif_thread_create("", &vm->tid, vm_run, vm, vm->opts) != 0) goto error; 208 | 209 | return vm; 210 | 211 | error: 212 | enif_release_resource(vm); 213 | return NULL; 214 | } 215 | 216 | void 217 | vm_destroy(ErlNifEnv* env, void* obj) 218 | { 219 | vm_ptr vm = (vm_ptr) obj; 220 | job_ptr job = job_create(); 221 | void* resp; 222 | 223 | assert(job != NULL && "Failed to create job."); 224 | job->type = job_close; 225 | queue_push(vm->jobs, job); 226 | queue_send(vm->jobs, job); 227 | 228 | enif_thread_join(vm->tid, &resp); 229 | 230 | queue_destroy(vm->jobs); 231 | enif_thread_opts_destroy(vm->opts); 232 | } 233 | 234 | void* 235 | vm_run(void* arg) 236 | { 237 | vm_ptr vm = (vm_ptr) arg; 238 | JSContext* cx; 239 | JSObject* gl; 240 | job_ptr job; 241 | ENTERM resp; 242 | int flags; 243 | 244 | cx = JS_NewContext(vm->runtime, vm->stack_size); 245 | if(cx == NULL) 246 | { 247 | fprintf(stderr, "Failed to create context.\n"); 248 | goto done; 249 | } 250 | 251 | JS_BeginRequest(cx); 252 | 253 | flags = 0; 254 | flags |= JSOPTION_VAROBJFIX; 255 | flags |= JSOPTION_STRICT; 256 | flags |= JSVERSION_LATEST; 257 | flags |= JSOPTION_COMPILE_N_GO; 258 | flags |= JSOPTION_XML; 259 | JS_SetOptions(cx, JS_GetOptions(cx) | flags); 260 | 261 | gl = JS_NewObject(cx, &global_class, NULL, NULL); 262 | if(gl == NULL) 263 | { 264 | fprintf(stderr, "Failed to create global object.\n"); 265 | goto done; 266 | } 267 | 268 | if(!JS_InitStandardClasses(cx, gl)) 269 | { 270 | fprintf(stderr, "Failed to initialize classes.\n"); 271 | goto done; 272 | } 273 | 274 | if(!install_jserl(cx, gl)) 275 | { 276 | fprintf(stderr, "Failed to install erlang object."); 277 | goto done; 278 | } 279 | 280 | JS_SetErrorReporter(cx, vm_report_error); 281 | JS_SetContextPrivate(cx, (void*) vm); 282 | 283 | JS_EndRequest(cx); 284 | 285 | while(1) 286 | { 287 | job = queue_pop(vm->jobs); 288 | if(job->type == job_close) 289 | { 290 | job_destroy(job); 291 | break; 292 | } 293 | 294 | JS_BeginRequest(cx); 295 | assert(vm->curr_job == NULL && "vm already has a job set."); 296 | vm->curr_job = job; 297 | 298 | if(job->type == job_eval) 299 | { 300 | resp = vm_eval(cx, gl, job); 301 | } 302 | else if(job->type == job_call) 303 | { 304 | resp = vm_call(cx, gl, job); 305 | } 306 | else 307 | { 308 | assert(0 && "Invalid job type."); 309 | } 310 | 311 | vm->curr_job = NULL; 312 | JS_EndRequest(cx); 313 | JS_MaybeGC(cx); 314 | 315 | // XXX: If pid is not alive, we just ignore it. 316 | enif_send(NULL, &(job->pid), job->env, resp); 317 | 318 | job_destroy(job); 319 | } 320 | 321 | done: 322 | JS_BeginRequest(cx); 323 | if(cx != NULL) JS_DestroyContext(cx); 324 | return NULL; 325 | } 326 | 327 | int 328 | vm_add_eval(vm_ptr vm, ENTERM ref, ENPID pid, ENBINARY bin) 329 | { 330 | job_ptr job = job_create(); 331 | 332 | job->type = job_eval; 333 | job->ref = enif_make_copy(job->env, ref); 334 | job->pid = pid; 335 | 336 | if(!enif_alloc_binary(bin.size, &(job->script))) goto error; 337 | memcpy(job->script.data, bin.data, bin.size); 338 | 339 | if(!queue_push(vm->jobs, job)) goto error; 340 | 341 | return 1; 342 | 343 | error: 344 | if(job != NULL) job_destroy(job); 345 | return 0; 346 | } 347 | 348 | int 349 | vm_add_call(vm_ptr vm, ENTERM ref, ENPID pid, ENTERM name, ENTERM args) 350 | { 351 | job_ptr job = job_create(); 352 | if(job == NULL) goto error; 353 | 354 | job->type = job_call; 355 | job->ref = enif_make_copy(job->env, ref); 356 | job->pid = pid; 357 | job->name = enif_make_copy(job->env, name); 358 | job->args = enif_make_copy(job->env, args); 359 | 360 | if(!queue_push(vm->jobs, job)) goto error; 361 | 362 | return 1; 363 | error: 364 | if(job != NULL) job_destroy(job); 365 | return 0; 366 | } 367 | 368 | int 369 | vm_send(vm_ptr vm, ENTERM data) 370 | { 371 | job_ptr job = job_create(); 372 | if(job == NULL) goto error; 373 | 374 | job->type = job_response; 375 | job->args = enif_make_copy(job->env, data); 376 | 377 | if(!queue_send(vm->jobs, job)) goto error; 378 | 379 | return 1; 380 | error: 381 | if(job != NULL) job_destroy(job); 382 | return 0; 383 | } 384 | 385 | ENTERM 386 | vm_eval(JSContext* cx, JSObject* gl, job_ptr job) 387 | { 388 | ENTERM resp; 389 | const char* script; 390 | size_t length; 391 | jsval rval; 392 | int cnt; 393 | int i; 394 | 395 | script = (const char*) job->script.data; 396 | length = job->script.size; 397 | 398 | for(i = 0, cnt = 0; i < length; i++) 399 | { 400 | if(script[i] == '\n') cnt += 1; 401 | } 402 | 403 | if(!JS_EvaluateScript(cx, gl, script, length, "", cnt, &rval)) 404 | { 405 | if(job->error != 0) 406 | { 407 | resp = vm_mk_error(job->env, job->error); 408 | } 409 | else 410 | { 411 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "unknown")); 412 | } 413 | } 414 | else 415 | { 416 | resp = vm_mk_ok(job->env, to_erl(job->env, cx, rval)); 417 | } 418 | 419 | return enif_make_tuple2(job->env, job->ref, resp); 420 | } 421 | 422 | ENTERM 423 | vm_call(JSContext* cx, JSObject* gl, job_ptr job) 424 | { 425 | ENTERM resp; 426 | ENTERM head; 427 | ENTERM tail; 428 | jsval func; 429 | jsval args[256]; 430 | jsval rval; 431 | jsid idp; 432 | int argc; 433 | 434 | // Get the function object. 435 | 436 | func = to_js(job->env, cx, job->name); 437 | if(func == JSVAL_VOID) 438 | { 439 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "invalid_name")); 440 | goto send; 441 | } 442 | 443 | if(!JS_ValueToId(cx, func, &idp)) 444 | { 445 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "internal_error")); 446 | goto send; 447 | } 448 | 449 | if(!JS_GetPropertyById(cx, gl, idp, &func)) 450 | { 451 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "bad_property")); 452 | goto send; 453 | } 454 | 455 | if(JS_TypeOfValue(cx, func) != JSTYPE_FUNCTION) 456 | { 457 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "not_a_function")); 458 | goto send; 459 | } 460 | 461 | // Creating function arguments. 462 | 463 | if(enif_is_empty_list(job->env, job->args)) 464 | { 465 | argc = 0; 466 | } 467 | else 468 | { 469 | if(!enif_get_list_cell(job->env, job->args, &head, &tail)) 470 | { 471 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "invalid_argv")); 472 | goto send; 473 | } 474 | 475 | argc = 0; 476 | do { 477 | args[argc++] = to_js(job->env, cx, head); 478 | } while(enif_get_list_cell(job->env, tail, &head, &tail) && argc < 256); 479 | } 480 | 481 | // Call function 482 | if(!JS_CallFunctionValue(cx, gl, func, argc, args, &rval)) 483 | { 484 | if(job->error != 0) 485 | { 486 | resp = vm_mk_error(job->env, job->error); 487 | } 488 | else 489 | { 490 | resp = vm_mk_error(job->env, util_mk_atom(job->env, "unknown")); 491 | } 492 | } 493 | else 494 | { 495 | resp = vm_mk_ok(job->env, to_erl(job->env, cx, rval)); 496 | } 497 | 498 | send: 499 | return enif_make_tuple2(job->env, job->ref, resp); 500 | } 501 | 502 | void 503 | vm_set_error(vm_ptr vm, ENBINARY mesg, ENBINARY src, unsigned int line) 504 | { 505 | ENTERM tmesg = enif_make_binary(vm->curr_job->env, &mesg); 506 | ENTERM tsrc = enif_make_binary(vm->curr_job->env, &src); 507 | ENTERM tline = enif_make_int(vm->curr_job->env, line); 508 | 509 | vm->curr_job->error = enif_make_tuple3( 510 | vm->curr_job->env, tmesg, tsrc, tline 511 | ); 512 | } 513 | 514 | void 515 | vm_report_error(JSContext* cx, const char* mesg, JSErrorReport* report) 516 | { 517 | vm_ptr vm; 518 | ErlNifBinary bmesg; 519 | ErlNifBinary bsrc; 520 | 521 | vm = (vm_ptr) JS_GetContextPrivate(cx); 522 | if(vm == NULL) return; 523 | 524 | if(!(report->flags & JSREPORT_EXCEPTION)) return; 525 | 526 | if(mesg == NULL) mesg = ""; 527 | if(report->linebuf == NULL) report->linebuf = ""; 528 | 529 | if(!enif_alloc_binary(strlen(mesg), &bmesg)) return; 530 | if(!enif_alloc_binary(strlen(report->linebuf), &bsrc)) return; 531 | 532 | memcpy(bmesg.data, mesg, strlen(mesg)); 533 | memcpy(bsrc.data, report->linebuf, strlen(report->linebuf)); 534 | 535 | vm_set_error(vm, bmesg, bsrc, report->lineno); 536 | } 537 | 538 | ENTERM 539 | vm_mk_ok(ErlNifEnv* env, ENTERM reason) 540 | { 541 | ENTERM ok = util_mk_atom(env, "ok"); 542 | return enif_make_tuple2(env, ok, reason); 543 | } 544 | 545 | ENTERM 546 | vm_mk_error(ErlNifEnv* env, ENTERM reason) 547 | { 548 | ENTERM error = util_mk_atom(env, "error"); 549 | return enif_make_tuple2(env, error, reason); 550 | } 551 | 552 | ENTERM 553 | vm_mk_message(ErlNifEnv* env, ENTERM data) 554 | { 555 | ENTERM message = util_mk_atom(env, "message"); 556 | return enif_make_tuple2(env, message, data); 557 | } 558 | -------------------------------------------------------------------------------- /c_src/vm.h: -------------------------------------------------------------------------------- 1 | // This file is part of Emonk released under the MIT license. 2 | // See the LICENSE file for more information. 3 | 4 | #ifndef EMONK_VM_H 5 | #define EMONK_VM_H 6 | 7 | #include 8 | #include "erl_nif.h" 9 | 10 | #include "alias.h" 11 | 12 | typedef struct vm_t* vm_ptr; 13 | 14 | vm_ptr vm_init(ErlNifResourceType* res_type, JSRuntime* runtime, size_t stack_size); 15 | void vm_destroy(ErlNifEnv* env, void* obj); 16 | 17 | int vm_add_eval(vm_ptr vm, ENTERM ref, ENPID pid, ENBINARY bin); 18 | int vm_add_call(vm_ptr vm, ENTERM ref, ENPID pid, ENTERM name, ENTERM args); 19 | int vm_send(vm_ptr vm, ENTERM data); 20 | 21 | #endif // Included vm.h 22 | -------------------------------------------------------------------------------- /docs/emonk.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davisp/emonk/1d313208abfbb8698ef953a8b223c106c4c87d94/docs/emonk.graffle -------------------------------------------------------------------------------- /docs/emonk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davisp/emonk/1d313208abfbb8698ef953a8b223c106c4c87d94/docs/emonk.png -------------------------------------------------------------------------------- /ebin/emonk.app: -------------------------------------------------------------------------------- 1 | {application, emonk, [ 2 | {description, "Emonk - JavaScript bindings for Erlang"}, 3 | {vsn, "0.0.0"}, 4 | {modules, [emonk]}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib]}, 7 | {env, []} 8 | ]}. 9 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davisp/emonk/1d313208abfbb8698ef953a8b223c106c4c87d94/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {port_sources, ["c_src/*.c"]}. 2 | {so_name, "emonk.so"}. 3 | 4 | {port_envs, [ 5 | %% Link the spidermonkey library 6 | {".*", "CFLAGS", "$CFLAGS -g -Wall"}, 7 | {".*", "LDFLAGS", "$LDFLAGS -lmozjs"}, 8 | 9 | %% Make sure to link -lstdc++ on linux or solaris 10 | {"(linux|solaris)", "LDFLAGS", "$LDFLAGS -lstdc++"}, 11 | 12 | %% OS X Leopard flags for 64-bit 13 | {"darwin9.*-64$", "CXXFLAGS", "-m64"}, 14 | {"darwin9.*-64$", "LDFLAGS", "-arch x86_64"}, 15 | 16 | %% OS X Snow Leopard flags for 32-bit 17 | {"darwin10.*-32$", "CXXFLAGS", "-m32"}, 18 | {"darwin10.*-32$", "LDFLAGS", "-arch i386"} 19 | ]}. 20 | -------------------------------------------------------------------------------- /src/emonk.erl: -------------------------------------------------------------------------------- 1 | % This file is part of Emonk released under the MIT license. 2 | % See the LICENSE file for more information. 3 | 4 | -module(emonk). 5 | -on_load(init/0). 6 | 7 | 8 | -export([create_ctx/0, create_ctx/1]). 9 | -export([eval/2, eval/3, call/3, call/4, send/3, send/4]). 10 | 11 | 12 | -define(APPNAME, emonk). 13 | -define(LIBNAME, emonk). 14 | -define(CTX_STACK, 8192). 15 | -define(TIMEOUT, infinity). 16 | 17 | create_ctx() -> 18 | create_ctx(?CTX_STACK). 19 | 20 | create_ctx(_) -> 21 | not_loaded(?LINE). 22 | 23 | 24 | eval(Ctx, Script) -> 25 | eval(Ctx, Script, ?TIMEOUT). 26 | 27 | eval(Ctx, Script, Timeout) -> 28 | Ref = make_ref(), 29 | ok = eval(Ctx, Ref, self(), Script), 30 | receive 31 | {Ref, Resp} -> 32 | Resp; 33 | {message, Resp} -> 34 | {message, Ref, Resp}; 35 | Other -> 36 | throw(Other) 37 | after Timeout -> 38 | throw({error, timeout, Ref}) 39 | end. 40 | 41 | eval(_Ctx, _Ref, _Dest, _Script) -> 42 | not_loaded(?LINE). 43 | 44 | 45 | call(Ctx, Name, Args) -> 46 | call(Ctx, Name, Args, ?TIMEOUT). 47 | 48 | call(Ctx, Name, Args, Timeout) -> 49 | Ref = make_ref(), 50 | ok = call(Ctx, Ref, self(), Name, Args), 51 | receive 52 | {Ref, Resp} -> 53 | Resp; 54 | {message, Resp} -> 55 | {message, Ref, Resp} 56 | after Timeout -> 57 | throw({error, timeout, Ref}) 58 | end. 59 | 60 | call(_Ctx, _Ref, _Dest, _Name, _Args) -> 61 | not_loaded(?LINE). 62 | 63 | send(_Ctx, _Data) -> 64 | not_loaded(?LINE). 65 | 66 | send(Ctx, Ref, Data) -> 67 | send(Ctx, Ref, Data, ?TIMEOUT). 68 | 69 | send(Ctx, Ref, Data, Timeout) -> 70 | ok = send(Ctx, Data), 71 | receive 72 | {Ref, Resp} -> 73 | Resp; 74 | {message, Resp} -> 75 | {message, Resp} 76 | after Timeout -> 77 | throw({error, timeout, Ref}) 78 | end. 79 | 80 | 81 | %% Internal API 82 | 83 | init() -> 84 | SoName = case code:priv_dir(?APPNAME) of 85 | {error, bad_name} -> 86 | case filelib:is_dir(filename:join(["..", priv])) of 87 | true -> 88 | filename:join(["..", priv, ?LIBNAME]); 89 | _ -> 90 | filename:join([priv, ?LIBNAME]) 91 | end; 92 | Dir -> 93 | filename:join(Dir, ?LIBNAME) 94 | end, 95 | erlang:load_nif(SoName, 0). 96 | 97 | 98 | not_loaded(Line) -> 99 | exit({not_loaded, [{module, ?MODULE}, {line, Line}]}). 100 | -------------------------------------------------------------------------------- /test/001-basics.t: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | main([]) -> 7 | test_util:run(length(modules()), fun() -> test() end). 8 | 9 | modules() -> 10 | [emonk]. 11 | 12 | test() -> 13 | lists:foreach(fun(Mod) -> 14 | Mesg = atom_to_list(Mod) ++ " module loaded", 15 | etap:loaded_ok(Mod, Mesg) 16 | end, modules()). 17 | 18 | -------------------------------------------------------------------------------- /test/003-test-ctx-creation.t: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | 7 | main([]) -> 8 | test_util:run(100, fun() -> test() end). 9 | 10 | test() -> 11 | lists:foreach(fun(_) -> test_vm_creation() end, lists:seq(1, 100, 1)), 12 | erlang:garbage_collect(). 13 | 14 | test_vm_creation() -> 15 | etap:fun_is( 16 | fun({ok, _}) -> true; (_) -> false end, 17 | emonk:create_ctx(), 18 | "Created a context successfully." 19 | ), 20 | erlang:garbage_collect(). 21 | 22 | -------------------------------------------------------------------------------- /test/004-basic-calls.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | main(_) -> 7 | test_util:run(10, fun() -> test() end). 8 | 9 | test() -> 10 | {ok, Ctx} = emonk:create_ctx(), 11 | 12 | test_eval_ok(Ctx), 13 | test_call_ok(Ctx), 14 | 15 | test_eval_undefined(Ctx), 16 | test_call_undefined(Ctx), 17 | 18 | test_eval_error(Ctx), 19 | test_call_error(Ctx). 20 | 21 | test_eval_ok(Ctx) -> 22 | etap:is( 23 | emonk:eval(Ctx, <<"var x = 2; x*3;">>), 24 | {ok, 6}, 25 | "Successful roundtrip through the JS vm." 26 | ). 27 | 28 | test_call_ok(Ctx) -> 29 | etap:fun_is( 30 | fun({ok, undefined}) -> true; (_) -> false end, 31 | emonk:eval(Ctx, <<"var g = function(x) {return x*2};">>), 32 | "Created function ok." 33 | ), 34 | 35 | etap:is( 36 | emonk:call(Ctx, <<"g">>, [6]), 37 | {ok, 12}, 38 | "Successful function call round trip with an argument string." 39 | ), 40 | 41 | etap:is( 42 | emonk:call(Ctx, <<"g">>, [600, foo]), 43 | {ok, 1200}, 44 | "Successful call roundtrip with an argument list." 45 | ). 46 | 47 | test_eval_undefined(Ctx) -> 48 | etap:is( 49 | emonk:eval(Ctx, <<"var x = function() {};">>), 50 | {ok, undefined}, 51 | "Successfully ignored non-JSON response." 52 | ). 53 | 54 | test_call_undefined(Ctx) -> 55 | etap:fun_is( 56 | fun({ok, undefined}) -> true; (_) -> false end, 57 | emonk:eval(Ctx, <<"var h = function(x) {return g};">>), 58 | "Created function ok." 59 | ), 60 | 61 | etap:is( 62 | emonk:call(Ctx, <<"h">>, []), 63 | {ok, undefined}, 64 | "Successfully ignored non-JSON response." 65 | ). 66 | 67 | test_eval_error(Ctx) -> 68 | etap:fun_is( 69 | fun({error, {_, _, _}}) -> true; (_E) -> throw(_E) end, 70 | emonk:eval(Ctx, <<"f * 3">>), 71 | "Reported the undefined error." 72 | ), 73 | 74 | etap:fun_is( 75 | fun({error, {_, _, _}}) -> true; (_) -> false end, 76 | emonk:eval(Ctx, <<"throw \"foo\";">>), 77 | "Reported the thrown exception." 78 | ). 79 | 80 | test_call_error(Ctx) -> 81 | {ok, undefined} = emonk:eval(Ctx, <<"var k = function(x) {throw(2);};">>), 82 | etap:fun_is( 83 | fun({error, {_, _, _}}) -> true; (_E) -> false end, 84 | emonk:call(Ctx, <<"k">>, []), 85 | "Reported a thrown error." 86 | ). 87 | -------------------------------------------------------------------------------- /test/005-erl-to-js.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | main(_) -> 7 | test_util:run(29, fun() -> test() end). 8 | 9 | 10 | test() -> 11 | {ok, Ctx} = emonk:create_ctx(), 12 | 13 | Tests = [ 14 | null, 15 | true, 16 | false, 17 | 1, 18 | -1, 19 | 3.1416, 20 | -3.1416, 21 | 12.0e10, 22 | 1.234E+10, 23 | -1.234E-10, 24 | 10.0, 25 | 123.456, 26 | 10.0, 27 | <<"foo">>, 28 | <<"foo", 5, "bar">>, 29 | <<"">>, 30 | <<"\n\n\n">>, 31 | <<"\" \b\f\r\n\t\"">>, 32 | {[]}, 33 | {[{<<"foo">>, <<"bar">>}]}, 34 | {[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, 35 | [], 36 | [[]], 37 | [1, <<"foo">>], 38 | {[{<<"foo">>, [123]}]}, 39 | {[{<<"foo">>, [1, 2, 3]}]}, 40 | {[{<<"foo">>, {[{<<"bar">>, true}]}}]}, 41 | {[ 42 | {<<"foo">>, []}, 43 | {<<"bar">>, {[{<<"baz">>, true}]}}, {<<"alice">>, <<"bob">>} 44 | ]}, 45 | [-123, <<"foo">>, {[{<<"bar">>, []}]}, null] 46 | ], 47 | run_tests(Ctx, Tests). 48 | 49 | run_tests(_, []) -> 50 | ok; 51 | run_tests(Ctx, [E1 | Tests]) -> 52 | E2 = sort(E1), 53 | {ok, undefined} = emonk:eval(Ctx, js()), 54 | Msg = io_lib:format("Roundtrip: ~p", [E2]), 55 | {ok, Result} = emonk:call(Ctx, <<"test">>, [E2]), 56 | etap:is(sort(Result), [E2], lists:flatten(Msg)), 57 | run_tests(Ctx, Tests). 58 | 59 | js() -> <<"var test = function(arg) {return [arg];};">>. 60 | 61 | % Sort this shit out so that altered object property 62 | % ordering doesnt make us evaluate inequal. 63 | % Arrays are not altered, just recursed through to 64 | % reach all objects. 65 | sort({Props}) -> 66 | objsort(Props, []); 67 | sort(List) when is_list(List) -> 68 | lstsort(List, []); 69 | sort(Other) -> 70 | Other. 71 | 72 | objsort([], Acc) -> 73 | {lists:sort(Acc)}; 74 | objsort([{K,V} | Rest], Acc) -> 75 | objsort(Rest, [{K, sort(V)} | Acc]). 76 | 77 | lstsort([], Acc) -> 78 | lists:reverse(Acc); 79 | lstsort([Val | Rest], Acc) -> 80 | lstsort(Rest, [sort(Val) | Acc]). 81 | -------------------------------------------------------------------------------- /test/006-js-to-erl.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | main(_) -> 7 | test_util:run(30, fun() -> test() end). 8 | 9 | test() -> 10 | {ok, Ctx} = emonk:create_ctx(), 11 | 12 | Tests = [ 13 | {<<"null">>, null}, 14 | {<<"true">>, true}, 15 | {<<"false">>, false}, 16 | {<<"1">>, 1}, 17 | {<<"-1">>, -1}, 18 | {<<"3.1416">>, 3.1416}, 19 | {<<"-3.1416">>, -3.1416}, 20 | {<<"12.0e10">>, 12.0e10}, 21 | {<<"1.234E+10">>, 1.234E+10}, 22 | {<<"-1.234E-10">>, -1.234E-10}, 23 | {<<"10.0">>, 10.0}, 24 | {<<"123.456">>, 123.456}, 25 | {<<"\"foo\"">>, <<"foo">>}, 26 | {<<"\"foo\\u0005bar\"">>, <<"foo", 5, "bar">>}, 27 | {<<"\"\"">>, <<"">>}, 28 | {<<"\"\\n\\n\\n\"">>, <<"\n\n\n">>}, 29 | {<<"\"\\\" \\b\\f\\r\\n\\t\\\"\"">>, <<"\" \b\f\r\n\t\"">>}, 30 | {<<"{}">>, {[]}}, 31 | {<<"{\"foo\": \"bar\"}">>, {[{<<"foo">>, <<"bar">>}]}}, 32 | { 33 | <<"{\"foo\": \"bar\", \"baz\": 123}">>, 34 | {[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]} 35 | }, 36 | {<<"[]">>, []}, 37 | {<<"[[]]">>, [[]]}, 38 | {<<"[1, \"foo\"]">>, [1, <<"foo">>]}, 39 | {<<"{\"foo\": [123]}">>, {[{<<"foo">>, [123]}]}}, 40 | {<<"{\"foo\": [1, 2, 3]}">>, {[{<<"foo">>, [1, 2, 3]}]}}, 41 | { 42 | <<"{\"foo\": {\"bar\": true}}">>, 43 | {[{<<"foo">>, {[{<<"bar">>, true}]}}]} 44 | }, 45 | { 46 | <<"{\"foo\": [], \"bar\": {\"baz\": true}, \"alice\": \"bob\"}">>, 47 | {[ 48 | {<<"foo">>, []}, 49 | {<<"bar">>, {[{<<"baz">>, true}]}}, {<<"alice">>, <<"bob">>} 50 | ]} 51 | }, 52 | { 53 | <<"[-123, \"foo\", {\"bar\": []}, null]">>, 54 | [-123, <<"foo">>, {[{<<"bar">>, []}]}, null] 55 | }, 56 | { 57 | <<"new Date(\"2003-03-01T17:13:34.001Z\")">>, 58 | <<"2003-03-01T17:13:34.001Z">> 59 | } 60 | ], 61 | ok = run_tests(Ctx, Tests), 62 | test_converter(Ctx). 63 | 64 | run_tests(_, []) -> 65 | ok; 66 | run_tests(Ctx, [{Arg, Exp} | Tests]) -> 67 | {ok, Result} = emonk:eval(Ctx, js(Arg)), 68 | Msg = io_lib:format("~p", [Arg]), 69 | etap:is(sort(Result), [sort(Exp)], lists:flatten(Msg)), 70 | run_tests(Ctx, Tests). 71 | 72 | test_converter(Ctx) -> 73 | Smoke = <<"Date.prototype.toJSON = function() {return {\"foo\": 2.1};};">>, 74 | Date = <<"new Date(\"2003-03-01T17:13:34.001Z\")">>, 75 | {ok, undefined} = emonk:eval(Ctx, Smoke), 76 | {ok, Result} = emonk:eval(Ctx, js(Date)), 77 | etap:is(sort(Result), [{[{<<"foo">>, 2.1}]}], "Is toJSON called?"), 78 | ok. 79 | 80 | js(Arg) -> <<"(function(arg) {return [arg];})(", Arg/binary, ");">>. 81 | 82 | % Sort this shit out so that altered object property 83 | % ordering doesn't make us evaluate inequal. 84 | % Arrays are not altered, just recursed through to 85 | % reach all objects. 86 | sort({Props}) -> 87 | objsort(Props, []); 88 | sort(List) when is_list(List) -> 89 | lstsort(List, []); 90 | sort(Other) -> 91 | Other. 92 | 93 | objsort([], Acc) -> 94 | {lists:sort(Acc)}; 95 | objsort([{K,V} | Rest], Acc) -> 96 | objsort(Rest, [{K, sort(V)} | Acc]). 97 | 98 | lstsort([], Acc) -> 99 | lists:reverse(Acc); 100 | lstsort([Val | Rest], Acc) -> 101 | lstsort(Rest, [sort(Val) | Acc]). 102 | -------------------------------------------------------------------------------- /test/007-js-send-message.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa ./test/ -pa ./ebin/ 3 | % This file is part of Emonk released under the MIT license. 4 | % See the LICENSE file for more information. 5 | 6 | main(_) -> 7 | test_util:run(unknown, fun() -> test() end). 8 | 9 | test() -> 10 | {ok, Ctx} = emonk:create_ctx(), 11 | test_send_exists(Ctx), 12 | test_send_message(Ctx), 13 | ok. 14 | 15 | test_send_exists(Ctx) -> 16 | JS = <<"var f = (erlang.send !== undefined); f;">>, 17 | etap:is(emonk:eval(Ctx, JS), {ok, true}, "erlang.send function exists."). 18 | 19 | test_send_message(Ctx) -> 20 | JS = <<"var f = erlang.send(1.3) == 2.6; f;">>, 21 | {message, Token, Data} = emonk:eval(Ctx, JS), 22 | etap:is(emonk:send(Ctx, Token, Data * 2), {ok, true}, "message passed ok"). 23 | -------------------------------------------------------------------------------- /test/etap.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008-2009 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% 24 | %% @author Nick Gerakines [http://socklabs.com/] 25 | %% @author Jeremy Wall 26 | %% @version 0.3.4 27 | %% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines 28 | %% @reference http://testanything.org/wiki/index.php/Main_Page 29 | %% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol 30 | %% @todo Finish implementing the skip directive. 31 | %% @todo Document the messages handled by this receive loop. 32 | %% @todo Explain in documentation why we use a process to handle test input. 33 | %% @doc etap is a TAP testing module for Erlang components and applications. 34 | %% This module allows developers to test their software using the TAP method. 35 | %% 36 | %%

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

42 | %% 43 | %% The testing process begins by defining a plan using etap:plan/1, running 44 | %% a number of etap tests and then calling eta:end_tests/0. Please refer to 45 | %% the Erlang modules in the t directory of this project for example tests. 46 | -module(etap). 47 | -vsn("0.3.4"). 48 | 49 | -export([ 50 | ensure_test_server/0, 51 | start_etap_server/0, 52 | test_server/1, 53 | msg/1, msg/2, 54 | diag/1, diag/2, 55 | expectation_mismatch_message/3, 56 | plan/1, 57 | end_tests/0, 58 | not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, 59 | fun_is/3, expect_fun/3, expect_fun/4, 60 | is_greater/3, 61 | skip/1, skip/2, 62 | datetime/1, 63 | skip/3, 64 | bail/0, bail/1, 65 | test_state/0, failure_count/0 66 | ]). 67 | 68 | -export([ 69 | contains_ok/3, 70 | is_before/4 71 | ]). 72 | 73 | -export([ 74 | is_pid/2, 75 | is_alive/2, 76 | is_mfa/3 77 | ]). 78 | 79 | -export([ 80 | loaded_ok/2, 81 | can_ok/2, can_ok/3, 82 | has_attrib/2, is_attrib/3, 83 | is_behaviour/2 84 | ]). 85 | 86 | -export([ 87 | dies_ok/2, 88 | lives_ok/2, 89 | throws_ok/3 90 | ]). 91 | 92 | 93 | -record(test_state, { 94 | planned = 0, 95 | count = 0, 96 | pass = 0, 97 | fail = 0, 98 | skip = 0, 99 | skip_reason = "" 100 | }). 101 | 102 | %% @spec plan(N) -> Result 103 | %% N = unknown | skip | {skip, string()} | integer() 104 | %% Result = ok 105 | %% @doc Create a test plan and boot strap the test server. 106 | plan(unknown) -> 107 | ensure_test_server(), 108 | etap_server ! {self(), plan, unknown}, 109 | ok; 110 | plan(skip) -> 111 | io:format("1..0 # skip~n"); 112 | plan({skip, Reason}) -> 113 | io:format("1..0 # skip ~s~n", [Reason]); 114 | plan(N) when is_integer(N), N > 0 -> 115 | ensure_test_server(), 116 | etap_server ! {self(), plan, N}, 117 | ok. 118 | 119 | %% @spec end_tests() -> ok 120 | %% @doc End the current test plan and output test results. 121 | %% @todo This should probably be done in the test_server process. 122 | end_tests() -> 123 | case whereis(etap_server) of 124 | undefined -> self() ! true; 125 | _ -> etap_server ! {self(), state} 126 | end, 127 | State = receive X -> X end, 128 | if 129 | State#test_state.planned == -1 -> 130 | io:format("1..~p~n", [State#test_state.count]); 131 | true -> 132 | ok 133 | end, 134 | case whereis(etap_server) of 135 | undefined -> ok; 136 | _ -> etap_server ! done, ok 137 | end. 138 | 139 | bail() -> 140 | bail(""). 141 | 142 | bail(Reason) -> 143 | etap_server ! {self(), diag, "Bail out! " ++ Reason}, 144 | etap_server ! done, ok, 145 | ok. 146 | 147 | %% @spec test_state() -> Return 148 | %% Return = test_state_record() | {error, string()} 149 | %% @doc Return the current test state 150 | test_state() -> 151 | etap_server ! {self(), state}, 152 | receive 153 | X when is_record(X, test_state) -> X 154 | after 155 | 1000 -> {error, "Timed out waiting for etap server reply.~n"} 156 | end. 157 | 158 | %% @spec failure_count() -> Return 159 | %% Return = integer() | {error, string()} 160 | %% @doc Return the current failure count 161 | failure_count() -> 162 | case test_state() of 163 | #test_state{fail=FailureCount} -> FailureCount; 164 | X -> X 165 | end. 166 | 167 | %% @spec msg(S) -> ok 168 | %% S = string() 169 | %% @doc Print a message in the test output. 170 | msg(S) -> etap_server ! {self(), diag, S}, ok. 171 | 172 | %% @spec msg(Format, Data) -> ok 173 | %% Format = atom() | string() | binary() 174 | %% Data = [term()] 175 | %% UnicodeList = [Unicode] 176 | %% Unicode = int() 177 | %% @doc Print a message in the test output. 178 | %% Function arguments are passed through io_lib:format/2. 179 | msg(Format, Data) -> msg(io_lib:format(Format, Data)). 180 | 181 | %% @spec diag(S) -> ok 182 | %% S = string() 183 | %% @doc Print a debug/status message related to the test suite. 184 | diag(S) -> msg("# " ++ S). 185 | 186 | %% @spec diag(Format, Data) -> ok 187 | %% Format = atom() | string() | binary() 188 | %% Data = [term()] 189 | %% UnicodeList = [Unicode] 190 | %% Unicode = int() 191 | %% @doc Print a debug/status message related to the test suite. 192 | %% Function arguments are passed through io_lib:format/2. 193 | diag(Format, Data) -> diag(io_lib:format(Format, Data)). 194 | 195 | %% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok 196 | %% Got = any() 197 | %% Expected = any() 198 | %% Desc = string() 199 | %% @doc Print an expectation mismatch message in the test output. 200 | expectation_mismatch_message(Got, Expected, Desc) -> 201 | msg(" ---"), 202 | msg(" description: ~p", [Desc]), 203 | msg(" found: ~p", [Got]), 204 | msg(" wanted: ~p", [Expected]), 205 | msg(" ..."), 206 | ok. 207 | 208 | % @spec evaluate(Pass, Got, Expected, Desc) -> Result 209 | %% Pass = true | false 210 | %% Got = any() 211 | %% Expected = any() 212 | %% Desc = string() 213 | %% Result = true | false 214 | %% @doc Evaluate a test statement, printing an expectation mismatch message 215 | %% if the test failed. 216 | evaluate(Pass, Got, Expected, Desc) -> 217 | case mk_tap(Pass, Desc) of 218 | false -> 219 | expectation_mismatch_message(Got, Expected, Desc), 220 | false; 221 | true -> 222 | true 223 | end. 224 | 225 | %% @spec ok(Expr, Desc) -> Result 226 | %% Expr = true | false 227 | %% Desc = string() 228 | %% Result = true | false 229 | %% @doc Assert that a statement is true. 230 | ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). 231 | 232 | %% @spec not_ok(Expr, Desc) -> Result 233 | %% Expr = true | false 234 | %% Desc = string() 235 | %% Result = true | false 236 | %% @doc Assert that a statement is false. 237 | not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). 238 | 239 | %% @spec is_ok(Expr, Desc) -> Result 240 | %% Expr = any() 241 | %% Desc = string() 242 | %% Result = true | false 243 | %% @doc Assert that two values are the same. 244 | is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). 245 | 246 | %% @spec is(Got, Expected, Desc) -> Result 247 | %% Got = any() 248 | %% Expected = any() 249 | %% Desc = string() 250 | %% Result = true | false 251 | %% @doc Assert that two values are the same. 252 | is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). 253 | 254 | %% @spec isnt(Got, Expected, Desc) -> Result 255 | %% Got = any() 256 | %% Expected = any() 257 | %% Desc = string() 258 | %% Result = true | false 259 | %% @doc Assert that two values are not the same. 260 | isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). 261 | 262 | %% @spec is_greater(ValueA, ValueB, Desc) -> Result 263 | %% ValueA = number() 264 | %% ValueB = number() 265 | %% Desc = string() 266 | %% Result = true | false 267 | %% @doc Assert that an integer is greater than another. 268 | is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> 269 | mk_tap(ValueA > ValueB, Desc). 270 | 271 | %% @spec any(Got, Items, Desc) -> Result 272 | %% Got = any() 273 | %% Items = [any()] 274 | %% Desc = string() 275 | %% Result = true | false 276 | %% @doc Assert that an item is in a list. 277 | any(Got, Items, Desc) when is_function(Got) -> 278 | is(lists:any(Got, Items), true, Desc); 279 | any(Got, Items, Desc) -> 280 | is(lists:member(Got, Items), true, Desc). 281 | 282 | %% @spec none(Got, Items, Desc) -> Result 283 | %% Got = any() 284 | %% Items = [any()] 285 | %% Desc = string() 286 | %% Result = true | false 287 | %% @doc Assert that an item is not in a list. 288 | none(Got, Items, Desc) when is_function(Got) -> 289 | is(lists:any(Got, Items), false, Desc); 290 | none(Got, Items, Desc) -> 291 | is(lists:member(Got, Items), false, Desc). 292 | 293 | %% @spec fun_is(Fun, Expected, Desc) -> Result 294 | %% Fun = function() 295 | %% Expected = any() 296 | %% Desc = string() 297 | %% Result = true | false 298 | %% @doc Use an anonymous function to assert a pattern match. 299 | fun_is(Fun, Expected, Desc) when is_function(Fun) -> 300 | is(Fun(Expected), true, Desc). 301 | 302 | %% @spec expect_fun(ExpectFun, Got, Desc) -> Result 303 | %% ExpectFun = function() 304 | %% Got = any() 305 | %% Desc = string() 306 | %% Result = true | false 307 | %% @doc Use an anonymous function to assert a pattern match, using actual 308 | %% value as the argument to the function. 309 | expect_fun(ExpectFun, Got, Desc) -> 310 | evaluate(ExpectFun(Got), Got, ExpectFun, Desc). 311 | 312 | %% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result 313 | %% ExpectFun = function() 314 | %% Got = any() 315 | %% Desc = string() 316 | %% ExpectStr = string() 317 | %% Result = true | false 318 | %% @doc Use an anonymous function to assert a pattern match, using actual 319 | %% value as the argument to the function. 320 | expect_fun(ExpectFun, Got, Desc, ExpectStr) -> 321 | evaluate(ExpectFun(Got), Got, ExpectStr, Desc). 322 | 323 | %% @equiv skip(TestFun, "") 324 | skip(TestFun) when is_function(TestFun) -> 325 | skip(TestFun, ""). 326 | 327 | %% @spec skip(TestFun, Reason) -> ok 328 | %% TestFun = function() 329 | %% Reason = string() 330 | %% @doc Skip a test. 331 | skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> 332 | begin_skip(Reason), 333 | catch TestFun(), 334 | end_skip(), 335 | ok. 336 | 337 | %% @spec skip(Q, TestFun, Reason) -> ok 338 | %% Q = true | false | function() 339 | %% TestFun = function() 340 | %% Reason = string() 341 | %% @doc Skips a test conditionally. The first argument to this function can 342 | %% either be the 'true' or 'false' atoms or a function that returns 'true' or 343 | %% 'false'. 344 | skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> 345 | case QFun() of 346 | true -> begin_skip(Reason), TestFun(), end_skip(); 347 | _ -> TestFun() 348 | end, 349 | ok; 350 | 351 | skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> 352 | begin_skip(Reason), 353 | TestFun(), 354 | end_skip(), 355 | ok; 356 | 357 | skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> 358 | TestFun(), 359 | ok. 360 | 361 | %% @private 362 | begin_skip(Reason) -> 363 | etap_server ! {self(), begin_skip, Reason}. 364 | 365 | %% @private 366 | end_skip() -> 367 | etap_server ! {self(), end_skip}. 368 | 369 | %% @spec contains_ok(string(), string(), string()) -> true | false 370 | %% @doc Assert that a string is contained in another string. 371 | contains_ok(Source, String, Desc) -> 372 | etap:isnt( 373 | string:str(Source, String), 374 | 0, 375 | Desc 376 | ). 377 | 378 | %% @spec is_before(string(), string(), string(), string()) -> true | false 379 | %% @doc Assert that a string comes before another string within a larger body. 380 | is_before(Source, StringA, StringB, Desc) -> 381 | etap:is_greater( 382 | string:str(Source, StringB), 383 | string:str(Source, StringA), 384 | Desc 385 | ). 386 | 387 | %% @doc Assert that a given variable is a pid. 388 | is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); 389 | is_pid(_, Desc) -> etap:ok(false, Desc). 390 | 391 | %% @doc Assert that a given process/pid is alive. 392 | is_alive(Pid, Desc) -> 393 | etap:ok(erlang:is_process_alive(Pid), Desc). 394 | 395 | %% @doc Assert that the current function of a pid is a given {M, F, A} tuple. 396 | is_mfa(Pid, MFA, Desc) -> 397 | etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). 398 | 399 | %% @spec loaded_ok(atom(), string()) -> true | false 400 | %% @doc Assert that a module has been loaded successfully. 401 | loaded_ok(M, Desc) when is_atom(M) -> 402 | etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). 403 | 404 | %% @spec can_ok(atom(), atom()) -> true | false 405 | %% @doc Assert that a module exports a given function. 406 | can_ok(M, F) when is_atom(M), is_atom(F) -> 407 | Matches = [X || {X, _} <- M:module_info(exports), X == F], 408 | etap:ok(Matches > 0, lists:concat([M, " can ", F])). 409 | 410 | %% @spec can_ok(atom(), atom(), integer()) -> true | false 411 | %% @doc Assert that a module exports a given function with a given arity. 412 | can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> 413 | Matches = [X || X <- M:module_info(exports), X == {F, A}], 414 | etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). 415 | 416 | %% @spec has_attrib(M, A) -> true | false 417 | %% M = atom() 418 | %% A = atom() 419 | %% @doc Asserts that a module has a given attribute. 420 | has_attrib(M, A) when is_atom(M), is_atom(A) -> 421 | etap:isnt( 422 | proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), 423 | 'asdlkjasdlkads', 424 | lists:concat([M, " has attribute ", A]) 425 | ). 426 | 427 | %% @spec has_attrib(M, A. V) -> true | false 428 | %% M = atom() 429 | %% A = atom() 430 | %% V = any() 431 | %% @doc Asserts that a module has a given attribute with a given value. 432 | is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> 433 | etap:is( 434 | proplists:get_value(A, M:module_info(attributes)), 435 | [V], 436 | lists:concat([M, "'s ", A, " is ", V]) 437 | ). 438 | 439 | %% @spec is_behavior(M, B) -> true | false 440 | %% M = atom() 441 | %% B = atom() 442 | %% @doc Asserts that a given module has a specific behavior. 443 | is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> 444 | is_attrib(M, behaviour, B). 445 | 446 | %% @doc Assert that an exception is raised when running a given function. 447 | dies_ok(F, Desc) -> 448 | case (catch F()) of 449 | {'EXIT', _} -> etap:ok(true, Desc); 450 | _ -> etap:ok(false, Desc) 451 | end. 452 | 453 | %% @doc Assert that an exception is not raised when running a given function. 454 | lives_ok(F, Desc) -> 455 | etap:is(try_this(F), success, Desc). 456 | 457 | %% @doc Assert that the exception thrown by a function matches the given exception. 458 | throws_ok(F, Exception, Desc) -> 459 | try F() of 460 | _ -> etap:ok(nok, Desc) 461 | catch 462 | _:E -> 463 | etap:is(E, Exception, Desc) 464 | end. 465 | 466 | %% @private 467 | %% @doc Run a function and catch any exceptions. 468 | try_this(F) when is_function(F, 0) -> 469 | try F() of 470 | _ -> success 471 | catch 472 | throw:E -> {throw, E}; 473 | error:E -> {error, E}; 474 | exit:E -> {exit, E} 475 | end. 476 | 477 | %% @private 478 | %% @doc Start the etap_server process if it is not running already. 479 | ensure_test_server() -> 480 | case whereis(etap_server) of 481 | undefined -> 482 | proc_lib:start(?MODULE, start_etap_server,[]); 483 | _ -> 484 | diag("The test server is already running.") 485 | end. 486 | 487 | %% @private 488 | %% @doc Start the etap_server loop and register itself as the etap_server 489 | %% process. 490 | start_etap_server() -> 491 | catch register(etap_server, self()), 492 | proc_lib:init_ack(ok), 493 | etap:test_server(#test_state{ 494 | planned = 0, 495 | count = 0, 496 | pass = 0, 497 | fail = 0, 498 | skip = 0, 499 | skip_reason = "" 500 | }). 501 | 502 | 503 | %% @private 504 | %% @doc The main etap_server receive/run loop. The etap_server receive loop 505 | %% responds to seven messages apperatining to failure or passing of tests. 506 | %% It is also used to initiate the testing process with the {_, plan, _} 507 | %% message that clears the current test state. 508 | test_server(State) -> 509 | NewState = receive 510 | {_From, plan, unknown} -> 511 | io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), 512 | io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), 513 | State#test_state{ 514 | planned = -1, 515 | count = 0, 516 | pass = 0, 517 | fail = 0, 518 | skip = 0, 519 | skip_reason = "" 520 | }; 521 | {_From, plan, N} -> 522 | io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), 523 | io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), 524 | io:format("1..~p~n", [N]), 525 | State#test_state{ 526 | planned = N, 527 | count = 0, 528 | pass = 0, 529 | fail = 0, 530 | skip = 0, 531 | skip_reason = "" 532 | }; 533 | {_From, begin_skip, Reason} -> 534 | State#test_state{ 535 | skip = 1, 536 | skip_reason = Reason 537 | }; 538 | {_From, end_skip} -> 539 | State#test_state{ 540 | skip = 0, 541 | skip_reason = "" 542 | }; 543 | {_From, pass, Desc} -> 544 | FullMessage = skip_diag( 545 | " - " ++ Desc, 546 | State#test_state.skip, 547 | State#test_state.skip_reason 548 | ), 549 | io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), 550 | State#test_state{ 551 | count = State#test_state.count + 1, 552 | pass = State#test_state.pass + 1 553 | }; 554 | 555 | {_From, fail, Desc} -> 556 | FullMessage = skip_diag( 557 | " - " ++ Desc, 558 | State#test_state.skip, 559 | State#test_state.skip_reason 560 | ), 561 | io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), 562 | State#test_state{ 563 | count = State#test_state.count + 1, 564 | fail = State#test_state.fail + 1 565 | }; 566 | {From, state} -> 567 | From ! State, 568 | State; 569 | {_From, diag, Message} -> 570 | io:format("~s~n", [Message]), 571 | State; 572 | {From, count} -> 573 | From ! State#test_state.count, 574 | State; 575 | {From, is_skip} -> 576 | From ! State#test_state.skip, 577 | State; 578 | done -> 579 | exit(normal) 580 | end, 581 | test_server(NewState). 582 | 583 | %% @private 584 | %% @doc Process the result of a test and send it to the etap_server process. 585 | mk_tap(Result, Desc) -> 586 | IsSkip = lib:sendw(etap_server, is_skip), 587 | case [IsSkip, Result] of 588 | [_, true] -> 589 | etap_server ! {self(), pass, Desc}, 590 | true; 591 | [1, _] -> 592 | etap_server ! {self(), pass, Desc}, 593 | true; 594 | _ -> 595 | etap_server ! {self(), fail, Desc}, 596 | false 597 | end. 598 | 599 | %% @private 600 | %% @doc Format a date/time string. 601 | datetime(DateTime) -> 602 | {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, 603 | 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]). 604 | 605 | %% @private 606 | %% @doc Craft an output message taking skip/todo into consideration. 607 | skip_diag(Message, 0, _) -> 608 | Message; 609 | skip_diag(_Message, 1, "") -> 610 | " # SKIP"; 611 | skip_diag(_Message, 1, Reason) -> 612 | " # SKIP : " ++ Reason. 613 | -------------------------------------------------------------------------------- /test/smoke/restarts.erl: -------------------------------------------------------------------------------- 1 | % This file is part of Emonk released under the MIT license. 2 | % See the LICENSE file for more information. 3 | 4 | -module(restarts). 5 | 6 | -export([run/0]). 7 | 8 | run(0) -> 9 | ok; 10 | run(N) -> 11 | {ok, Ctx} = emonk:create_ctx(), 12 | emonk:eval(Ctx, <<"var f = function(x) {return x*3;};">>), 13 | {ok, 27} = emonk:call(Ctx, <<"f">>, [9]), 14 | run(N-1). 15 | 16 | run() -> 17 | io:format(".", []), 18 | code:add_path("ebin"), 19 | run(5), 20 | init:restart(). 21 | 22 | -------------------------------------------------------------------------------- /test/smoke/segfault.es: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env escript 2 | % This file is part of Emonk released under the MIT license. 3 | % See the LICENSE file for more information. 4 | 5 | main([]) -> 6 | run(). 7 | 8 | do_context(Script) -> 9 | {ok, Ctx} = emonk:create_ctx(), 10 | emonk:eval(Ctx, <<"var process={};var f = ", Script/binary, " f(process);">>), 11 | {ok, Resp} = emonk:eval(Ctx, <<"process.add_message('test');process.next_message();">>), 12 | <<"test">> = Resp. 13 | 14 | create_context(0, _Script) -> 15 | ok; 16 | create_context(N, Script) -> 17 | do_context(Script), 18 | io:format("Num: ~p\n", [N]), 19 | create_context(N-1, Script). 20 | 21 | run() -> 22 | code:add_path("ebin"), 23 | Script = <<" 24 | (function(process) { 25 | 26 | var processQueue = []; 27 | 28 | process.next_message = function() { 29 | return processQueue.pop() 30 | } 31 | 32 | process.add_message = function(message) { 33 | processQueue.push(message); 34 | } 35 | 36 | function log(message) { 37 | process.add_message(['log', message]) 38 | } 39 | 40 | log('test'); 41 | 42 | }); 43 | ">>, 44 | create_context(100000, Script). 45 | -------------------------------------------------------------------------------- /test/smoke/separate-contexts.es: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env escript 2 | % This file is part of Emonk released under the MIT license. 3 | % See the LICENSE file for more information. 4 | 5 | main([]) -> 6 | start(64, 1000); 7 | main([N]) -> 8 | start(list_to_integer(N), 1000); 9 | main([N, M]) -> 10 | start(list_to_integer(N), list_to_integer(M)). 11 | 12 | 13 | start(N, M) -> 14 | code:add_pathz("test"), 15 | code:add_pathz("ebin"), 16 | run(N, M), 17 | wait(N). 18 | 19 | run(0, _) -> 20 | ok; 21 | run(N, M) -> 22 | {ok, Ctx} = emonk:create_ctx(), 23 | {ok, undefined} = emonk:eval(Ctx, js()), 24 | Self = self(), 25 | Pid = spawn(fun() -> do_js(Self, Ctx, M) end), 26 | io:format("Spawned: ~p~n", [Pid]), 27 | run(N-1, M). 28 | 29 | wait(0) -> 30 | ok; 31 | wait(N) -> 32 | receive 33 | {finished, Pid} -> ok 34 | end, 35 | io:format("Finished: ~p~n", [Pid]), 36 | wait(N-1). 37 | 38 | do_js(Parent, _, 0) -> 39 | Parent ! {finished, self()}; 40 | do_js(Parent, Ctx, M) -> 41 | io:format("Running: ~p~n", [M]), 42 | Test = random_test(), 43 | {ok, [Resp]} = emonk:call(Ctx, <<"f">>, [Test]), 44 | Sorted = sort(Resp), 45 | true = Test == Sorted, 46 | do_js(Parent, Ctx, M-1). 47 | 48 | js() -> 49 | <<"var f = function(x) {return [x];};">>. 50 | 51 | random_test() -> 52 | Tests = [ 53 | null, 54 | true, 55 | false, 56 | 1, 57 | -1, 58 | 3.1416, 59 | -3.1416, 60 | 12.0e10, 61 | 1.234E+10, 62 | -1.234E-10, 63 | 10.0, 64 | 123.456, 65 | 10.0, 66 | <<"foo">>, 67 | <<"foo", 5, "bar">>, 68 | <<"">>, 69 | <<"\n\n\n">>, 70 | <<"\" \b\f\r\n\t\"">>, 71 | {[]}, 72 | {[{<<"foo">>, <<"bar">>}]}, 73 | {[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, 74 | [], 75 | [[]], 76 | [1, <<"foo">>], 77 | {[{<<"foo">>, [123]}]}, 78 | {[{<<"foo">>, [1, 2, 3]}]}, 79 | {[{<<"foo">>, {[{<<"bar">>, true}]}}]}, 80 | {[ 81 | {<<"foo">>, []}, 82 | {<<"bar">>, {[{<<"baz">>, true}]}}, {<<"alice">>, <<"bob">>} 83 | ]}, 84 | [-123, <<"foo">>, {[{<<"bar">>, []}]}, null] 85 | ], 86 | {_, [Test | _]} = lists:split(random:uniform(length(Tests)) - 1, Tests), 87 | sort(Test). 88 | 89 | sort({Props}) -> 90 | objsort(Props, []); 91 | sort(List) when is_list(List) -> 92 | lstsort(List, []); 93 | sort(Other) -> 94 | Other. 95 | 96 | objsort([], Acc) -> 97 | {lists:sort(Acc)}; 98 | objsort([{K,V} | Rest], Acc) -> 99 | objsort(Rest, [{K, sort(V)} | Acc]). 100 | 101 | lstsort([], Acc) -> 102 | lists:reverse(Acc); 103 | lstsort([Val | Rest], Acc) -> 104 | lstsort(Rest, [sort(Val) | Acc]). 105 | -------------------------------------------------------------------------------- /test/smoke/single-context.es: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env escript 2 | % This file is part of Emonk released under the MIT license. 3 | % See the LICENSE file for more information. 4 | 5 | main([]) -> 6 | start(64, 1000); 7 | main([N]) -> 8 | start(list_to_integer(N), 1000); 9 | main([N, M]) -> 10 | start(list_to_integer(N), list_to_integer(M)). 11 | 12 | 13 | start(N, M) -> 14 | code:add_pathz("test"), 15 | code:add_pathz("ebin"), 16 | {ok, Ctx} = emonk:create_ctx(), 17 | {ok, undefined} = emonk:eval(Ctx, js()), 18 | run(Ctx, N, M), 19 | wait(N). 20 | 21 | run(_, 0, _) -> 22 | ok; 23 | run(Ctx, N, M) -> 24 | Self = self(), 25 | Pid = spawn(fun() -> do_js(Self, Ctx, M) end), 26 | io:format("Spawned: ~p~n", [Pid]), 27 | run(Ctx, N-1, M). 28 | 29 | wait(0) -> 30 | ok; 31 | wait(N) -> 32 | receive 33 | {finished, Pid} -> ok 34 | end, 35 | io:format("Finished: ~p~n", [Pid]), 36 | wait(N-1). 37 | 38 | do_js(Parent, _, 0) -> 39 | Parent ! {finished, self()}; 40 | do_js(Parent, Ctx, M) -> 41 | io:format("Running: ~p~n", [M]), 42 | Test = random_test(), 43 | {ok, [Resp]} = emonk:call(Ctx, <<"f">>, [Test]), 44 | Sorted = sort(Resp), 45 | true = Test == Sorted, 46 | do_js(Parent, Ctx, M-1). 47 | 48 | js() -> 49 | <<"var f = function(x) {return [x];};">>. 50 | 51 | random_test() -> 52 | Tests = [ 53 | null, 54 | true, 55 | false, 56 | 1, 57 | -1, 58 | 3.1416, 59 | -3.1416, 60 | 12.0e10, 61 | 1.234E+10, 62 | -1.234E-10, 63 | 10.0, 64 | 123.456, 65 | 10.0, 66 | <<"foo">>, 67 | <<"foo", 5, "bar">>, 68 | <<"">>, 69 | <<"\n\n\n">>, 70 | <<"\" \b\f\r\n\t\"">>, 71 | {[]}, 72 | {[{<<"foo">>, <<"bar">>}]}, 73 | {[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, 74 | [], 75 | [[]], 76 | [1, <<"foo">>], 77 | {[{<<"foo">>, [123]}]}, 78 | {[{<<"foo">>, [1, 2, 3]}]}, 79 | {[{<<"foo">>, {[{<<"bar">>, true}]}}]}, 80 | {[ 81 | {<<"foo">>, []}, 82 | {<<"bar">>, {[{<<"baz">>, true}]}}, {<<"alice">>, <<"bob">>} 83 | ]}, 84 | [-123, <<"foo">>, {[{<<"bar">>, []}]}, null] 85 | ], 86 | {_, [Test | _]} = lists:split(random:uniform(length(Tests)) - 1, Tests), 87 | sort(Test). 88 | 89 | sort({Props}) -> 90 | objsort(Props, []); 91 | sort(List) when is_list(List) -> 92 | lstsort(List, []); 93 | sort(Other) -> 94 | Other. 95 | 96 | objsort([], Acc) -> 97 | {lists:sort(Acc)}; 98 | objsort([{K,V} | Rest], Acc) -> 99 | objsort(Rest, [{K, sort(V)} | Acc]). 100 | 101 | lstsort([], Acc) -> 102 | lists:reverse(Acc); 103 | lstsort([Val | Rest], Acc) -> 104 | lstsort(Rest, [sort(Val) | Acc]). 105 | -------------------------------------------------------------------------------- /test/test_util.erl: -------------------------------------------------------------------------------- 1 | % This file is part of Emonk released under the MIT license. 2 | % See the LICENSE file for more information. 3 | -module(test_util). 4 | 5 | -export([run/2]). 6 | 7 | run(Plan, Fun) -> 8 | etap:plan(Plan), 9 | try 10 | Fun(), 11 | etap:end_tests() 12 | catch 13 | Type:Error -> 14 | etap:diag("Test died abnormally: ~p:~p", [Type, Error]), 15 | etap:diag("~p", [erlang:get_stacktrace()]), 16 | etap:bail() 17 | end. 18 | 19 | --------------------------------------------------------------------------------