[FL-2887] actions unit tests runner (#1920)

Co-authored-by: Konstantin Volkov <k.volkov@flipperdevices.com>
Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
Konstantin Volkov 2022-10-28 16:59:09 +03:00 committed by GitHub
parent be3ee9f2fe
commit 492f147568
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 0 deletions

56
.github/workflows/unit_tests.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: 'Unit tests'
on:
pull_request:
env:
TARGETS: f7
DEFAULT_TARGET: f7
jobs:
main:
runs-on: [self-hosted, FlipperZeroTest]
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: 'Get flipper from device manager (mock)'
id: device
run: |
echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
- name: 'Compile unit tests firmware'
id: compile
continue-on-error: true
run: |
FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
- name: 'Wait for flipper to finish updating'
id: connect
if: steps.compile.outcome == 'success'
continue-on-error: true
run: |
python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
- name: 'Format flipper SD card'
id: format
if: steps.connect.outcome == 'success'
continue-on-error: true
run: |
./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
- name: 'Copy unit tests to flipper'
id: copy
if: steps.format.outcome == 'success'
continue-on-error: true
run: |
./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests
- name: 'Run units and validate results'
if: steps.copy.outcome == 'success'
continue-on-error: true
run: |
python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}}

View File

@ -340,6 +340,19 @@ class FlipperStorage:
else: else:
return True return True
def format_ext(self):
"""Create a directory on Flipper"""
self.send_and_wait_eol("storage format /ext\r")
self.send_and_wait_eol("y\r")
answer = self.read.until(self.CLI_EOL)
self.read.until(self.CLI_PROMPT)
if self.has_error(answer):
self.last_error = self.get_error(answer)
return False
else:
return True
def remove(self, path): def remove(self, path):
"""Remove file or directory on Flipper""" """Remove file or directory on Flipper"""
self.send_and_wait_eol('storage remove "' + path + '"\r') self.send_and_wait_eol('storage remove "' + path + '"\r')

View File

@ -21,6 +21,11 @@ class Main(App):
self.parser_mkdir.add_argument("flipper_path", help="Flipper path") self.parser_mkdir.add_argument("flipper_path", help="Flipper path")
self.parser_mkdir.set_defaults(func=self.mkdir) self.parser_mkdir.set_defaults(func=self.mkdir)
self.parser_format = self.subparsers.add_parser(
"format_ext", help="Format flash card"
)
self.parser_format.set_defaults(func=self.format_ext)
self.parser_remove = self.subparsers.add_parser( self.parser_remove = self.subparsers.add_parser(
"remove", help="Remove file/directory" "remove", help="Remove file/directory"
) )
@ -275,6 +280,17 @@ class Main(App):
storage.stop() storage.stop()
return 0 return 0
def format_ext(self):
if not (storage := self._get_storage()):
return 1
self.logger.debug("Formatting /ext SD card")
if not storage.format_ext():
self.logger.error(f"Error: {storage.last_error}")
storage.stop()
return 0
def stress(self): def stress(self):
self.logger.error("This test is wearing out flash memory.") self.logger.error("This test is wearing out flash memory.")
self.logger.error("Never use it with internal storage(/int)") self.logger.error("Never use it with internal storage(/int)")

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
import sys, os, time
def flp_serial_by_name(flp_name):
if sys.platform == "darwin": # MacOS
flp_serial = "/dev/cu.usbmodemflip_" + flp_name + "1"
elif sys.platform == "linux": # Linux
flp_serial = (
"/dev/serial/by-id/usb-Flipper_Devices_Inc._Flipper_"
+ flp_name
+ "_flip_"
+ flp_name
+ "-if00"
)
if os.path.exists(flp_serial):
return flp_serial
else:
if os.path.exists(flp_name):
return flp_name
else:
return ""
UPDATE_TIMEOUT = 30
def main():
flipper_name = sys.argv[1]
elapsed = 0
flipper = flp_serial_by_name(flipper_name)
while flipper == "" and elapsed < UPDATE_TIMEOUT:
elapsed += 1
time.sleep(1)
flipper = flp_serial_by_name(flipper_name)
if flipper == "":
print(f"Cannot find {flipper_name} flipper. Guess your flipper swam away")
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()

79
scripts/testing/units.py Normal file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env python3
import sys, os
import serial
import re
from await_flipper import flp_serial_by_name
LEAK_THRESHOLD = 3000 # added until units are fixed
def main():
flp_serial = flp_serial_by_name(sys.argv[1])
if flp_serial == "":
print("Name or serial port is invalid")
sys.exit(1)
with serial.Serial(flp_serial, timeout=1) as flipper:
flipper.baudrate = 230400
flipper.flushOutput()
flipper.flushInput()
flipper.timeout = 300
flipper.read_until(b">: ").decode("utf-8")
flipper.write(b"unit_tests\r")
data = flipper.read_until(b">: ").decode("utf-8")
lines = data.split("\r\n")
tests_re = r"Failed tests: \d{0,}"
time_re = r"Consumed: \d{0,}"
leak_re = r"Leaked: \d{0,}"
status_re = r"Status: \w{3,}"
tests_pattern = re.compile(tests_re)
time_pattern = re.compile(time_re)
leak_pattern = re.compile(leak_re)
status_pattern = re.compile(status_re)
tests, time, leak, status = None, None, None, None
for line in lines:
print(line)
if not tests:
tests = re.match(tests_pattern, line)
if not time:
time = re.match(time_pattern, line)
if not leak:
leak = re.match(leak_pattern, line)
if not status:
status = re.match(status_pattern, line)
if leak is None or time is None or leak is None or status is None:
print("Failed to get data. Or output is corrupt")
sys.exit(1)
leak = int(re.findall(r"[- ]\d+", leak.group(0))[0])
status = re.findall(r"\w+", status.group(0))[1]
tests = int(re.findall(r"\d+", tests.group(0))[0])
time = int(re.findall(r"\d+", time.group(0))[0])
if tests > 0 or leak > LEAK_THRESHOLD or status != "PASSED":
print(f"Got {tests} failed tests.")
print(f"Leaked {leak} bytes.")
print(f"Status by flipper: {status}")
print(f"Time elapsed {time/1000} seconds.")
sys.exit(1)
print(
f"Tests ran successfully! Time elapsed {time/1000} seconds. Passed {tests} tests."
)
sys.exit(0)
if __name__ == "__main__":
main()