Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FL-3893] JS modules #3841

Merged
merged 71 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
7d42728
feat: backport js_gpio from unleashed
portasynthinca3 Aug 14, 2024
f6c3bfe
feat: backport js_keyboard, TextInputModel::minimum_length from unlea…
portasynthinca3 Aug 14, 2024
549b982
fix: api version inconsistency
portasynthinca3 Aug 14, 2024
05cb9d8
style: js_gpio
portasynthinca3 Aug 14, 2024
c076027
build: fix submodule ._ .
portasynthinca3 Aug 14, 2024
d8bc740
refactor: js_gpio
portasynthinca3 Aug 15, 2024
847cc70
docs: type declarations for gpio
portasynthinca3 Aug 15, 2024
24f4f59
feat: gpio interrupts
portasynthinca3 Aug 16, 2024
6c2efd7
fix: js_gpio freeing, resetting and minor stylistic changes
portasynthinca3 Aug 16, 2024
d4a3f4f
style: js_gpio
portasynthinca3 Aug 16, 2024
fbc3c49
style: mlib array, fixme's
portasynthinca3 Aug 19, 2024
d42be45
feat: js_gpio adc
portasynthinca3 Aug 19, 2024
2e93a57
feat: js_event_loop
portasynthinca3 Aug 20, 2024
3591e38
docs: js_event_loop
portasynthinca3 Aug 20, 2024
354191a
feat: js_event_loop subscription cancellation
portasynthinca3 Aug 21, 2024
371d9fb
feat: js_event_loop + js_gpio integration
portasynthinca3 Aug 21, 2024
b331f23
fix: js_event_loop memory leak
portasynthinca3 Aug 22, 2024
c0a6538
feat: stop event loop on back button
portasynthinca3 Aug 22, 2024
b788863
test: js: basic, math, event_loop
portasynthinca3 Aug 23, 2024
5b932cc
feat: js_event_loop queue
portasynthinca3 Aug 26, 2024
12b9f7f
feat: js linkage to previously loaded plugins
portasynthinca3 Sep 3, 2024
acb415a
build: fix ci errors
portasynthinca3 Sep 3, 2024
38dcc07
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 3, 2024
78185db
feat: js module ordered teardown
portasynthinca3 Sep 4, 2024
98e19c2
feat: js_gui_defer_free
portasynthinca3 Sep 4, 2024
332fc97
feat: basic hourglass view
portasynthinca3 Sep 5, 2024
9cbe1c5
style: JS ASS (Argument Schema for Scripts)
portasynthinca3 Sep 5, 2024
67417de
fix: js_event_loop mem leaks and lifetime problems
portasynthinca3 Sep 5, 2024
43dc0e9
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 5, 2024
85d6007
fix: crashing test and pvs false positives
portasynthinca3 Sep 5, 2024
3a44627
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 6, 2024
121f065
feat: mjs custom obj destructors, gui submenu view
portasynthinca3 Sep 8, 2024
8323fcd
refactor: yank js_gui_defer_free (yuck)
portasynthinca3 Sep 8, 2024
876de7f
refactor: maybe_unsubscribe
portasynthinca3 Sep 8, 2024
953f08d
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 8, 2024
12d6c36
empty_screen, docs, typing fix-ups
portasynthinca3 Sep 8, 2024
3e60796
docs: navigation event & demo
portasynthinca3 Sep 9, 2024
aec2e8f
feat: submenu setHeader
portasynthinca3 Sep 9, 2024
5adb694
feat: text_input
portasynthinca3 Sep 9, 2024
74258db
feat: text_box
portasynthinca3 Sep 9, 2024
e259bc7
docs: text_box availability
portasynthinca3 Sep 9, 2024
fe58f5b
ci: silence irrelevant pvs low priority warning
portasynthinca3 Sep 9, 2024
65bda4c
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 9, 2024
21283aa
Merge remote-tracking branch 'origin/dev' into portasynthinca3/3893-j…
skotopes Sep 12, 2024
297bcb0
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Sep 17, 2024
82b49de
style: use furistring
portasynthinca3 Sep 17, 2024
1cffd14
style: _get_at -> _safe_get
portasynthinca3 Sep 17, 2024
3af4fc8
fix: built-in module name assignment
portasynthinca3 Sep 17, 2024
f61a725
feat: js_dialog; refactor, optimize: js_gui
portasynthinca3 Sep 19, 2024
af6c617
docs: js_gui
portasynthinca3 Sep 19, 2024
66dfe5f
ci: silence pvs warning
portasynthinca3 Sep 19, 2024
e06eedc
style: fix storage spelling
portasynthinca3 Sep 25, 2024
07f0bf3
feat: foreign pointer signature checks
portasynthinca3 Sep 25, 2024
35860ca
feat: js_storage
portasynthinca3 Oct 1, 2024
f58013d
docs: js_storage
portasynthinca3 Oct 1, 2024
9b76a8d
fix: my unit test was breaking other tests ;_;
portasynthinca3 Oct 1, 2024
0236464
ci: fix ci?
portasynthinca3 Oct 1, 2024
4ed0987
Merge branch 'dev' into portasynthinca3/3893-js-backport
skotopes Oct 6, 2024
9e5068d
Merge remote-tracking branch 'origin/dev' into portasynthinca3/3893-j…
skotopes Oct 10, 2024
75ce762
Make doxygen happy
skotopes Oct 10, 2024
9e0b3eb
docs: flipper, math, notification, global
portasynthinca3 Oct 11, 2024
7d4357b
style: review suggestions
portasynthinca3 Oct 11, 2024
18a64e9
style: review fixups
portasynthinca3 Oct 14, 2024
f0383fa
fix: badusb demo script
portasynthinca3 Oct 14, 2024
3bc048a
docs: badusb
portasynthinca3 Oct 14, 2024
315df52
Merge branch 'dev' into portasynthinca3/3893-js-backport
portasynthinca3 Oct 14, 2024
7725538
ci: add nofl
portasynthinca3 Oct 14, 2024
e1879a8
Merge branch 'portasynthinca3/3893-js-backport' of https://github.com…
portasynthinca3 Oct 14, 2024
02d4bb6
ci: make linter happy
portasynthinca3 Oct 14, 2024
5173a0e
Merge branch 'dev' into portasynthinca3/3893-js-backport
skotopes Oct 14, 2024
70dbb41
Bump api version
skotopes Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions applications/debug/unit_tests/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ App(
requires=["unit_tests"],
)

App(
appid="test_js",
sources=["tests/common/*.c", "tests/js/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests", "js_app"],
)

App(
appid="test_strint",
sources=["tests/common/*.c", "tests/strint/*.c"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let tests = require("tests");

tests.assert_eq(1337, 1337);
tests.assert_eq("hello", "hello");
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
let tests = require("tests");
let event_loop = require("event_loop");

let ext = {
i: 0,
received: false,
};

let queue = event_loop.queue(16);

event_loop.subscribe(queue.input, function (_, item, tests, ext) {
tests.assert_eq(123, item);
ext.received = true;
}, tests, ext);

event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) {
ext.i++;
queue.send(123);
if (counter === 10)
event_loop.stop();
return [queue, counter + 1, ext];
}, queue, 1, ext);

event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) {
tests.fail("event loop was not stopped");
}, tests);

event_loop.run();
tests.assert_eq(10, ext.i);
tests.assert_eq(true, ext.received);
34 changes: 34 additions & 0 deletions applications/debug/unit_tests/resources/unit_tests/js/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
let tests = require("tests");
let math = require("math");

// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16

// basics
tests.assert_float_close(5, math.abs(-5), math.EPSILON);
tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON);
tests.assert_float_close(5, math.abs(5), math.EPSILON);
tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON);
tests.assert_float_close(3, math.cbrt(27), math.EPSILON);
tests.assert_float_close(6, math.ceil(5.3), math.EPSILON);
tests.assert_float_close(31, math.clz32(1), math.EPSILON);
tests.assert_float_close(5, math.floor(5.7), math.EPSILON);
tests.assert_float_close(5, math.max(3, 5), math.EPSILON);
tests.assert_float_close(3, math.min(3, 5), math.EPSILON);
tests.assert_float_close(-1, math.sign(-5), math.EPSILON);
tests.assert_float_close(5, math.trunc(5.7), math.EPSILON);

// trig
tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON);
tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON);
tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON);
tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON);
tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON);
tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON);
tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON);
tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15
tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16

// powers
tests.assert_float_close(5, math.sqrt(25), math.EPSILON);
tests.assert_float_close(8, math.pow(2, 3), math.EPSILON);
tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16
136 changes: 136 additions & 0 deletions applications/debug/unit_tests/resources/unit_tests/js/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
let storage = require("storage");
let tests = require("tests");

let baseDir = "/ext/.tmp/unit_tests";

tests.assert_eq(true, storage.rmrf(baseDir));
tests.assert_eq(true, storage.makeDirectory(baseDir));

// write
let file = storage.openFile(baseDir + "/helloworld", "w", "create_always");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.write("Hello, World!"));
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());

// read
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.size());
tests.assert_eq("Hello, World!", file.read("ascii", 128));
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());

// seek
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.size());
tests.assert_eq("Hello, World!", file.read("ascii", 128));
tests.assert_eq(true, file.seekAbsolute(1));
tests.assert_eq(true, file.seekRelative(2));
tests.assert_eq(3, file.tell());
tests.assert_eq(false, file.eof());
tests.assert_eq("lo, World!", file.read("ascii", 128));
tests.assert_eq(true, file.eof());
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());

// byte-level copy
let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always");
tests.assert_eq(true, !!src);
tests.assert_eq(true, src.isOpen());
tests.assert_eq(true, !!dst);
tests.assert_eq(true, dst.isOpen());
tests.assert_eq(true, src.copyTo(dst, 10));
tests.assert_eq(true, dst.seekAbsolute(0));
tests.assert_eq("Hello, Wor", dst.read("ascii", 128));
tests.assert_eq(true, src.copyTo(dst, 3));
tests.assert_eq(true, dst.seekAbsolute(0));
tests.assert_eq("Hello, World!", dst.read("ascii", 128));
tests.assert_eq(true, src.eof());
tests.assert_eq(true, src.close());
tests.assert_eq(false, src.isOpen());
tests.assert_eq(true, dst.eof());
tests.assert_eq(true, dst.close());
tests.assert_eq(false, dst.isOpen());

// truncate
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2"));
file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.seekAbsolute(5));
tests.assert_eq(true, file.truncate());
tests.assert_eq(true, file.close());
file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq("Hello", file.read("ascii", 128));
tests.assert_eq(true, file.close());

// existence
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2"));
tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123"));
tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir));
tests.assert_eq(true, storage.directoryExists(baseDir));
tests.assert_eq(true, storage.fileOrDirExists(baseDir));
tests.assert_eq(true, storage.remove(baseDir + "/helloworld2"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2"));

// stat
let stat = storage.stat(baseDir + "/helloworld");
tests.assert_eq(true, !!stat);
tests.assert_eq(baseDir + "/helloworld", stat.path);
tests.assert_eq(false, stat.isDirectory);
tests.assert_eq(13, stat.size);

// rename
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));

// copy
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));

// next avail
tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20));

// fs info
let fsInfo = storage.fsInfo("/ext");
tests.assert_eq(true, !!fsInfo);
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/
fsInfo = storage.fsInfo("/int");
tests.assert_eq(true, !!fsInfo);
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace);

// path operations
tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test"));
tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt"));
tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub"));
tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test"));

// dir
let entries = storage.readDirectory(baseDir);
tests.assert_eq(true, !!entries);
// FIXME: (-nofl) this test suite assumes that files are listed by
// `readDirectory` in the exact order that they were created, which is not
// something that is actually guaranteed.
// Possible solution: sort and compare the array.
tests.assert_eq("helloworld", entries[0].path);
tests.assert_eq("helloworld123", entries[1].path);

tests.assert_eq(true, storage.rmrf(baseDir));
tests.assert_eq(true, storage.makeDirectory(baseDir));
88 changes: 88 additions & 0 deletions applications/debug/unit_tests/tests/js/js_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "../test.h" // IWYU pragma: keep

#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_random.h>

#include <storage/storage.h>
#include <applications/system/js_app/js_thread.h>

#include <stdint.h>

#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")

typedef enum {
JsTestsFinished = 1,
JsTestsError = 2,
} JsTestFlag;

typedef struct {
FuriEventFlag* event_flags;
FuriString* error_string;
} JsTestCallbackContext;

static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
JsTestCallbackContext* context = param;
if(event == JsThreadEventPrint) {
FURI_LOG_I("js_test", "%s", msg);
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
context->error_string = furi_string_alloc_set_str(msg);
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
} else if(event == JsThreadEventDone) {
furi_event_flag_set(context->event_flags, JsTestsFinished);
}
}

static void js_test_run(const char* script_path) {
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
context->event_flags = furi_event_flag_alloc();

JsThread* thread = js_thread_run(script_path, js_test_callback, context);
uint32_t flags = furi_event_flag_wait(
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
if(flags & FuriFlagError) {
// getting the flags themselves should not fail
furi_crash();
}

FuriString* error_string = context->error_string;

js_thread_stop(thread);
furi_event_flag_free(context->event_flags);
free(context);

if(flags & JsTestsError) {
// memory leak: not freeing the FuriString if the tests fail,
// because mu_fail executes a return
//
// who cares tho?
mu_fail(furi_string_get_cstr(error_string));
}
}

MU_TEST(js_test_basic) {
js_test_run(JS_SCRIPT_PATH("basic"));
}
MU_TEST(js_test_math) {
js_test_run(JS_SCRIPT_PATH("math"));
}
MU_TEST(js_test_event_loop) {
js_test_run(JS_SCRIPT_PATH("event_loop"));
}
MU_TEST(js_test_storage) {
js_test_run(JS_SCRIPT_PATH("storage"));
}

MU_TEST_SUITE(test_js) {
MU_RUN_TEST(js_test_basic);
MU_RUN_TEST(js_test_math);
MU_RUN_TEST(js_test_event_loop);
MU_RUN_TEST(js_test_storage);
}

int run_minunit_test_js(void) {
MU_RUN_SUITE(test_js);
return MU_EXIT_CODE;
}

TEST_API_DEFINE(run_minunit_test_js)
5 changes: 3 additions & 2 deletions applications/debug/unit_tests/tests/minunit.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extern "C" {
#include <Windows.h>
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#define __func__ __FUNCTION__
#define __func__ __FUNCTION__ //-V1059
#endif

#elif defined(__unix__) || defined(__unix) || defined(unix) || \
Expand All @@ -56,7 +56,7 @@ extern "C" {
#endif

#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
#define __func__ __extension__ __FUNCTION__
#define __func__ __extension__ __FUNCTION__ //-V1059
#endif

#else
Expand Down Expand Up @@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...);
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)

/* Test runner */
//-V:MU_RUN_TEST:550
#define MU_RUN_TEST(test) \
MU__SAFE_BLOCK( \
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \
Expand Down
14 changes: 5 additions & 9 deletions applications/debug/unit_tests/unit_test_api_table_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#include <rpc/rpc_i.h>
#include <flipper.pb.h>
#include <core/event_loop.h>
#include <applications/system/js_app/js_thread.h>

static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
Expand All @@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
xQueueGenericSend,
BaseType_t,
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)),
API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)),
API_METHOD(
furi_event_loop_subscribe_message_queue,
void,
(FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)),
API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)),
API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)),
API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)),
js_thread_run,
JsThread*,
(const char* script_path, JsThreadCallback callback, void* context)),
API_METHOD(js_thread_stop, void, (JsThread * worker)),
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
Loading
Loading