Skip to content

Commit c191116

Browse files
Merge branch 'dev' into portasynthinca3/3940-js-sdk-serial
2 parents 6e21efc + 8dd5e64 commit c191116

File tree

3 files changed

+131
-127
lines changed

3 files changed

+131
-127
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

scripts/testops.py

Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#!/usr/bin/env python3
2-
32
import re
4-
import sys
53
import time
4+
from datetime import datetime
65
from typing import Optional
76

87
from flipper.app import App
@@ -11,7 +10,10 @@
1110

1211

1312
class Main(App):
14-
# this is basic use without sub-commands, simply to reboot flipper / power it off, not meant as a full CLI wrapper
13+
def __init__(self, no_exit=False):
14+
super().__init__(no_exit)
15+
self.test_results = None
16+
1517
def init(self):
1618
self.parser.add_argument("-p", "--port", help="CDC Port", default="auto")
1719
self.parser.add_argument(
@@ -67,64 +69,108 @@ def run_units(self):
6769
self.logger.info("Running unit tests")
6870
flipper.send("unit_tests" + "\r")
6971
self.logger.info("Waiting for unit tests to complete")
70-
data = flipper.read.until(">: ")
71-
self.logger.info("Parsing result")
72-
73-
lines = data.decode().split("\r\n")
74-
75-
tests_re = r"Failed tests: \d{0,}"
76-
time_re = r"Consumed: \d{0,}"
77-
leak_re = r"Leaked: \d{0,}"
78-
status_re = r"Status: \w{3,}"
79-
80-
tests_pattern = re.compile(tests_re)
81-
time_pattern = re.compile(time_re)
82-
leak_pattern = re.compile(leak_re)
83-
status_pattern = re.compile(status_re)
8472

8573
tests, elapsed_time, leak, status = None, None, None, None
8674
total = 0
87-
88-
for line in lines:
89-
self.logger.info(line)
90-
if "()" in line:
91-
total += 1
92-
93-
if not tests:
94-
tests = re.match(tests_pattern, line)
95-
if not elapsed_time:
96-
elapsed_time = re.match(time_pattern, line)
97-
if not leak:
98-
leak = re.match(leak_pattern, line)
99-
if not status:
100-
status = re.match(status_pattern, line)
101-
102-
if None in (tests, elapsed_time, leak, status):
103-
self.logger.error(
104-
f"Failed to parse output: {tests} {elapsed_time} {leak} {status}"
75+
all_required_found = False
76+
77+
full_output = []
78+
79+
tests_pattern = re.compile(r"Failed tests: \d{0,}")
80+
time_pattern = re.compile(r"Consumed: \d{0,}")
81+
leak_pattern = re.compile(r"Leaked: \d{0,}")
82+
status_pattern = re.compile(r"Status: \w{3,}")
83+
84+
try:
85+
while not all_required_found:
86+
try:
87+
line = flipper.read.until("\r\n", cut_eol=True).decode()
88+
self.logger.info(line)
89+
if "command not found," in line:
90+
self.logger.error(f"Command not found: {line}")
91+
return 1
92+
93+
if "()" in line:
94+
total += 1
95+
self.logger.debug(f"Test completed: {line}")
96+
97+
if not tests:
98+
tests = tests_pattern.match(line)
99+
if not elapsed_time:
100+
elapsed_time = time_pattern.match(line)
101+
if not leak:
102+
leak = leak_pattern.match(line)
103+
if not status:
104+
status = status_pattern.match(line)
105+
106+
pattern = re.compile(
107+
r"(\[-]|\[\\]|\[\|]|\[/-]|\[[^\]]*\]|\x1b\[\d+D)"
108+
)
109+
line_to_append = pattern.sub("", line)
110+
pattern = re.compile(r"\[3D[^\]]*")
111+
line_to_append = pattern.sub("", line_to_append)
112+
line_to_append = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')} {line_to_append}"
113+
114+
full_output.append(line_to_append)
115+
116+
if tests and elapsed_time and leak and status:
117+
all_required_found = True
118+
try:
119+
remaining = flipper.read.until(">: ", cut_eol=True).decode()
120+
if remaining.strip():
121+
full_output.append(remaining)
122+
except:
123+
pass
124+
break
125+
126+
except Exception as e:
127+
self.logger.error(f"Error reading output: {e}")
128+
raise
129+
130+
if None in (tests, elapsed_time, leak, status):
131+
raise RuntimeError(
132+
f"Failed to parse output: {tests} {elapsed_time} {leak} {status}"
133+
)
134+
135+
leak = int(re.findall(r"[- ]\d+", leak.group(0))[0])
136+
status = re.findall(r"\w+", status.group(0))[1]
137+
tests = int(re.findall(r"\d+", tests.group(0))[0])
138+
elapsed_time = int(re.findall(r"\d+", elapsed_time.group(0))[0])
139+
140+
test_results = {
141+
"full_output": "\n".join(full_output),
142+
"total_tests": total,
143+
"failed_tests": tests,
144+
"elapsed_time_ms": elapsed_time,
145+
"memory_leak_bytes": leak,
146+
"status": status,
147+
}
148+
149+
self.test_results = test_results
150+
151+
output_file = "unit_tests_output.txt"
152+
with open(output_file, "w") as f:
153+
f.write(test_results["full_output"])
154+
155+
print(
156+
f"::notice:: Total tests: {total} Failed tests: {tests} Status: {status} Elapsed time: {elapsed_time / 1000} s Memory leak: {leak} bytes"
105157
)
106-
sys.exit(1)
107-
108-
leak = int(re.findall(r"[- ]\d+", leak.group(0))[0])
109-
status = re.findall(r"\w+", status.group(0))[1]
110-
tests = int(re.findall(r"\d+", tests.group(0))[0])
111-
elapsed_time = int(re.findall(r"\d+", elapsed_time.group(0))[0])
112-
113-
if tests > 0 or status != "PASSED":
114-
self.logger.error(f"Got {tests} failed tests.")
115-
self.logger.error(f"Leaked (not failing on this stat): {leak}")
116-
self.logger.error(f"Status: {status}")
117-
self.logger.error(f"Time: {elapsed_time/1000} seconds")
118-
flipper.stop()
119-
return 1
120158

121-
self.logger.info(f"Leaked (not failing on this stat): {leak}")
122-
self.logger.info(
123-
f"Tests ran successfully! Time elapsed {elapsed_time/1000} seconds. Passed {total} tests."
124-
)
159+
if tests > 0 or status != "PASSED":
160+
self.logger.error(f"Got {tests} failed tests.")
161+
self.logger.error(f"Leaked (not failing on this stat): {leak}")
162+
self.logger.error(f"Status: {status}")
163+
self.logger.error(f"Time: {elapsed_time / 1000} seconds")
164+
return 1
125165

126-
flipper.stop()
127-
return 0
166+
self.logger.info(f"Leaked (not failing on this stat): {leak}")
167+
self.logger.info(
168+
f"Tests ran successfully! Time elapsed {elapsed_time / 1000} seconds. Passed {total} tests."
169+
)
170+
return 0
171+
172+
finally:
173+
flipper.stop()
128174

129175

130176
if __name__ == "__main__":

0 commit comments

Comments
 (0)