Skip to content

Commit e0c34ce

Browse files
authored
Merge branch 'dev' into unlint-js-pkgs
2 parents 05660e2 + a02781b commit e0c34ce

File tree

36 files changed

+707
-187
lines changed

36 files changed

+707
-187
lines changed

.github/workflows/unit_tests.yml

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,64 +5,56 @@ on:
55
env:
66
TARGETS: f7
77
DEFAULT_TARGET: f7
8-
FBT_TOOLCHAIN_PATH: /opt
8+
FBT_TOOLCHAIN_PATH: /opt/
99
FBT_GIT_SUBMODULE_SHALLOW: 1
1010

1111
jobs:
1212
run_units_on_bench:
13-
runs-on: [self-hosted, FlipperZeroUnitTest]
13+
runs-on: [ self-hosted, FlipperZeroTest ]
1414
steps:
15-
- name: 'Wipe workspace'
16-
run: find ./ -mount -maxdepth 1 -exec rm -rf {} \;
17-
1815
- name: Checkout code
1916
uses: actions/checkout@v4
2017
with:
2118
fetch-depth: 1
2219
ref: ${{ github.event.pull_request.head.sha }}
2320

24-
- name: 'Get flipper from device manager (mock)'
25-
id: device
26-
run: |
27-
echo "flipper=auto" >> $GITHUB_OUTPUT
28-
2921
- name: 'Flash unit tests firmware'
3022
id: flashing
3123
if: success()
32-
timeout-minutes: 10
33-
run: |
34-
./fbt resources firmware_latest flash SWD_TRANSPORT_SERIAL=2A0906016415303030303032 LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1
35-
36-
- name: 'Wait for flipper and format ext'
37-
id: format_ext
38-
if: steps.flashing.outcome == 'success'
39-
timeout-minutes: 5
24+
timeout-minutes: 20
4025
run: |
4126
source scripts/toolchain/fbtenv.sh
42-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=120 await_flipper
43-
python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
27+
./fbt resources firmware_latest flash LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1
28+
4429
4530
- name: 'Copy assets and unit data, reboot and wait for flipper'
4631
id: copy
47-
if: steps.format_ext.outcome == 'success'
32+
if: steps.flashing.outcome == 'success'
4833
timeout-minutes: 7
4934
run: |
5035
source scripts/toolchain/fbtenv.sh
51-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=15 await_flipper
52-
rm -rf build/latest/resources/dolphin
53-
python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send build/latest/resources /ext
54-
python3 scripts/power.py -p ${{steps.device.outputs.flipper}} reboot
55-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=15 await_flipper
36+
python3 scripts/testops.py -t=15 await_flipper
37+
python3 scripts/storage.py -f send build/latest/resources /ext
38+
python3 scripts/storage.py -f send /region_data /ext/.int/.region_data
39+
python3 scripts/power.py reboot
40+
python3 scripts/testops.py -t=30 await_flipper
5641
5742
- name: 'Run units and validate results'
5843
id: run_units
5944
if: steps.copy.outcome == 'success'
6045
timeout-minutes: 7
6146
run: |
6247
source scripts/toolchain/fbtenv.sh
63-
python3 scripts/testops.py run_units -p ${{steps.device.outputs.flipper}}
48+
python3 scripts/testops.py run_units
49+
50+
- name: 'Upload test results'
51+
if: failure() && steps.flashing.outcome == 'success' && steps.run_units.outcome != 'skipped'
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: unit-tests_output
55+
path: unit_tests*.txt
6456

6557
- name: 'Check GDB output'
6658
if: failure() && steps.flashing.outcome == 'success'
6759
run: |
68-
./fbt gdb_trace_all SWD_TRANSPORT_SERIAL=2A0906016415303030303032 LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1
60+
./fbt gdb_trace_all LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1

.github/workflows/updater_test.yml

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,39 @@
11
name: 'Updater test'
22
on:
33
pull_request:
4+
45
env:
56
TARGETS: f7
67
DEFAULT_TARGET: f7
7-
FBT_TOOLCHAIN_PATH: /opt
8+
FBT_TOOLCHAIN_PATH: /opt/
89
FBT_GIT_SUBMODULE_SHALLOW: 1
910

1011
jobs:
1112
test_updater_on_bench:
12-
runs-on: [self-hosted, FlipperZeroUpdaterTest]
13+
runs-on: [self-hosted, FlipperZeroTest ]
1314
steps:
14-
- name: 'Wipe workspace'
15-
run: find ./ -mount -maxdepth 1 -exec rm -rf {} \;
16-
1715
- name: Checkout code
1816
uses: actions/checkout@v4
1917
with:
2018
fetch-depth: 1
21-
submodules: false
2219
ref: ${{ github.event.pull_request.head.sha }}
2320

24-
- name: 'Get flipper from device manager (mock)'
25-
id: device
26-
run: |
27-
echo "flipper=auto" >> $GITHUB_OUTPUT
28-
echo "stlink=0F020D026415303030303032" >> $GITHUB_OUTPUT
29-
3021
- name: 'Flashing target firmware'
3122
id: first_full_flash
32-
timeout-minutes: 10
23+
timeout-minutes: 20
3324
run: |
3425
source scripts/toolchain/fbtenv.sh
35-
./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
36-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=180 await_flipper
26+
python3 scripts/testops.py -t=180 await_flipper
27+
./fbt flash_usb_full FORCE=1
28+
3729
3830
- name: 'Validating updater'
3931
id: second_full_flash
4032
timeout-minutes: 10
4133
if: success()
4234
run: |
4335
source scripts/toolchain/fbtenv.sh
44-
./fbt flash_usb PORT=${{steps.device.outputs.flipper}} FORCE=1
45-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=180 await_flipper
46-
47-
- name: 'Get last release tag'
48-
id: release_tag
49-
if: failure()
50-
run: |
51-
echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT
52-
53-
- name: 'Wipe workspace'
54-
run: find ./ -mount -maxdepth 1 -exec rm -rf {} \;
36+
python3 scripts/testops.py -t=180 await_flipper
37+
./fbt flash_usb FORCE=1
38+
python3 scripts/testops.py -t=180 await_flipper
5539
56-
- name: 'Checkout latest release'
57-
uses: actions/checkout@v4
58-
if: failure()
59-
with:
60-
fetch-depth: 1
61-
ref: ${{ steps.release_tag.outputs.tag }}
62-
63-
- name: 'Flash last release'
64-
if: failure()
65-
run: |
66-
./fbt flash SWD_TRANSPORT_SERIAL=${{steps.device.outputs.stlink}} FORCE=1
67-
68-
- name: 'Wait for flipper and format ext'
69-
if: failure()
70-
run: |
71-
source scripts/toolchain/fbtenv.sh
72-
python3 scripts/testops.py -p=${{steps.device.outputs.flipper}} -t=180 await_flipper
73-
python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#include <furi.h>
2+
#include <errno.h>
3+
#include <stdio.h>
4+
#include "../test.h" // IWYU pragma: keep
5+
6+
#define TAG "StdioTest"
7+
8+
#define CONTEXT_MAGIC ((void*)0xDEADBEEF)
9+
10+
// stdin
11+
12+
static char mock_in[256];
13+
static size_t mock_in_len, mock_in_pos;
14+
15+
static void set_mock_in(const char* str) {
16+
size_t len = strlen(str);
17+
strcpy(mock_in, str);
18+
mock_in_len = len;
19+
mock_in_pos = 0;
20+
}
21+
22+
static size_t mock_in_cb(char* buffer, size_t size, FuriWait wait, void* context) {
23+
UNUSED(wait);
24+
furi_check(context == CONTEXT_MAGIC);
25+
size_t remaining = mock_in_len - mock_in_pos;
26+
size = MIN(remaining, size);
27+
memcpy(buffer, mock_in + mock_in_pos, size);
28+
mock_in_pos += size;
29+
return size;
30+
}
31+
32+
void test_stdin(void) {
33+
FuriThreadStdinReadCallback in_cb = furi_thread_get_stdin_callback();
34+
furi_thread_set_stdin_callback(mock_in_cb, CONTEXT_MAGIC);
35+
char buf[256];
36+
37+
// plain in
38+
set_mock_in("Hello, World!\n");
39+
fgets(buf, sizeof(buf), stdin);
40+
mu_assert_string_eq("Hello, World!\n", buf);
41+
mu_assert_int_eq(EOF, getchar());
42+
43+
// ungetc
44+
ungetc('i', stdin);
45+
ungetc('H', stdin);
46+
fgets(buf, sizeof(buf), stdin);
47+
mu_assert_string_eq("Hi", buf);
48+
mu_assert_int_eq(EOF, getchar());
49+
50+
// ungetc + plain in
51+
set_mock_in(" World");
52+
ungetc('i', stdin);
53+
ungetc('H', stdin);
54+
fgets(buf, sizeof(buf), stdin);
55+
mu_assert_string_eq("Hi World", buf);
56+
mu_assert_int_eq(EOF, getchar());
57+
58+
// partial plain in
59+
set_mock_in("Hello, World!\n");
60+
fgets(buf, strlen("Hello") + 1, stdin);
61+
mu_assert_string_eq("Hello", buf);
62+
mu_assert_int_eq(',', getchar());
63+
fgets(buf, sizeof(buf), stdin);
64+
mu_assert_string_eq(" World!\n", buf);
65+
66+
furi_thread_set_stdin_callback(in_cb, CONTEXT_MAGIC);
67+
}
68+
69+
// stdout
70+
71+
static FuriString* mock_out;
72+
FuriThreadStdoutWriteCallback original_out_cb;
73+
74+
static void mock_out_cb(const char* data, size_t size, void* context) {
75+
furi_check(context == CONTEXT_MAGIC);
76+
// there's no furi_string_cat_strn :(
77+
for(size_t i = 0; i < size; i++) {
78+
furi_string_push_back(mock_out, data[i]);
79+
}
80+
}
81+
82+
static void assert_and_clear_mock_out(const char* expected) {
83+
// return the original stdout callback for the duration of the check
84+
// if the check fails, we don't want the error to end up in our buffer,
85+
// we want to be able to see it!
86+
furi_thread_set_stdout_callback(original_out_cb, CONTEXT_MAGIC);
87+
mu_assert_string_eq(expected, furi_string_get_cstr(mock_out));
88+
furi_thread_set_stdout_callback(mock_out_cb, CONTEXT_MAGIC);
89+
90+
furi_string_reset(mock_out);
91+
}
92+
93+
void test_stdout(void) {
94+
original_out_cb = furi_thread_get_stdout_callback();
95+
furi_thread_set_stdout_callback(mock_out_cb, CONTEXT_MAGIC);
96+
mock_out = furi_string_alloc();
97+
98+
puts("Hello, World!");
99+
assert_and_clear_mock_out("Hello, World!\n");
100+
101+
printf("He");
102+
printf("llo!");
103+
fflush(stdout);
104+
assert_and_clear_mock_out("Hello!");
105+
106+
furi_string_free(mock_out);
107+
furi_thread_set_stdout_callback(original_out_cb, CONTEXT_MAGIC);
108+
}

applications/debug/unit_tests/tests/furi/furi_test.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ void test_furi_memmgr(void);
1010
void test_furi_event_loop(void);
1111
void test_errno_saving(void);
1212
void test_furi_primitives(void);
13+
void test_stdin(void);
14+
void test_stdout(void);
1315

1416
static int foo = 0;
1517

@@ -52,6 +54,11 @@ MU_TEST(mu_test_furi_primitives) {
5254
test_furi_primitives();
5355
}
5456

57+
MU_TEST(mu_test_stdio) {
58+
test_stdin();
59+
test_stdout();
60+
}
61+
5562
MU_TEST_SUITE(test_suite) {
5663
MU_SUITE_CONFIGURE(&test_setup, &test_teardown);
5764
MU_RUN_TEST(test_check);
@@ -61,6 +68,7 @@ MU_TEST_SUITE(test_suite) {
6168
MU_RUN_TEST(mu_test_furi_pubsub);
6269
MU_RUN_TEST(mu_test_furi_memmgr);
6370
MU_RUN_TEST(mu_test_furi_event_loop);
71+
MU_RUN_TEST(mu_test_stdio);
6472
MU_RUN_TEST(mu_test_errno_saving);
6573
MU_RUN_TEST(mu_test_furi_primitives);
6674
}

applications/main/infrared/infrared_app.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ static void infrared_rpc_command_callback(const RpcAppSystemEvent* event, void*
8888
view_dispatcher_send_custom_event(
8989
infrared->view_dispatcher, InfraredCustomEventTypeRpcButtonPressIndex);
9090
}
91+
} else if(event->type == RpcAppEventTypeButtonPressRelease) {
92+
furi_assert(
93+
event->data.type == RpcAppSystemEventDataTypeString ||
94+
event->data.type == RpcAppSystemEventDataTypeInt32);
95+
if(event->data.type == RpcAppSystemEventDataTypeString) {
96+
furi_string_set(infrared->button_name, event->data.string);
97+
view_dispatcher_send_custom_event(
98+
infrared->view_dispatcher, InfraredCustomEventTypeRpcButtonPressReleaseName);
99+
} else {
100+
infrared->app_state.current_button_index = event->data.i32;
101+
view_dispatcher_send_custom_event(
102+
infrared->view_dispatcher, InfraredCustomEventTypeRpcButtonPressReleaseIndex);
103+
}
91104
} else if(event->type == RpcAppEventTypeButtonRelease) {
92105
view_dispatcher_send_custom_event(
93106
infrared->view_dispatcher, InfraredCustomEventTypeRpcButtonRelease);
@@ -411,6 +424,26 @@ void infrared_tx_stop(InfraredApp* infrared) {
411424
infrared->app_state.last_transmit_time = furi_get_tick();
412425
}
413426

427+
void infrared_tx_send_once(InfraredApp* infrared) {
428+
if(infrared->app_state.is_transmitting) {
429+
return;
430+
}
431+
432+
dolphin_deed(DolphinDeedIrSend);
433+
infrared_signal_transmit(infrared->current_signal);
434+
}
435+
436+
InfraredErrorCode infrared_tx_send_once_button_index(InfraredApp* infrared, size_t button_index) {
437+
furi_assert(button_index < infrared_remote_get_signal_count(infrared->remote));
438+
439+
InfraredErrorCode error = infrared_remote_load_signal(
440+
infrared->remote, infrared->current_signal, infrared->app_state.current_button_index);
441+
if(!INFRARED_ERROR_PRESENT(error)) {
442+
infrared_tx_send_once(infrared);
443+
}
444+
445+
return error;
446+
}
414447
void infrared_blocking_task_start(InfraredApp* infrared, FuriThreadCallback callback) {
415448
view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewLoading);
416449
furi_thread_set_callback(infrared->task_thread, callback);

applications/main/infrared/infrared_app_i.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,20 @@ InfraredErrorCode infrared_tx_start_button_index(InfraredApp* infrared, size_t b
218218
*/
219219
void infrared_tx_stop(InfraredApp* infrared);
220220

221+
/**
222+
* @brief Transmit the currently loaded signal once.
223+
*
224+
* @param[in,out] infrared pointer to the application instance.
225+
*/
226+
void infrared_tx_send_once(InfraredApp* infrared);
227+
228+
/**
229+
* @brief Load the signal under the given index and transmit it once.
230+
*
231+
* @param[in,out] infrared pointer to the application instance.
232+
*/
233+
InfraredErrorCode infrared_tx_send_once_button_index(InfraredApp* infrared, size_t button_index);
234+
221235
/**
222236
* @brief Start a blocking task in a separate thread.
223237
*

applications/main/infrared/infrared_custom_event.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ enum InfraredCustomEventType {
2121
InfraredCustomEventTypeRpcButtonPressName,
2222
InfraredCustomEventTypeRpcButtonPressIndex,
2323
InfraredCustomEventTypeRpcButtonRelease,
24+
InfraredCustomEventTypeRpcButtonPressReleaseName,
25+
InfraredCustomEventTypeRpcButtonPressReleaseIndex,
2426
InfraredCustomEventTypeRpcSessionClose,
2527

2628
InfraredCustomEventTypeGpioTxPinChanged,

0 commit comments

Comments
 (0)