Преглед изворни кода

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

Co-authored-by: Konstantin Volkov <k.volkov@flipperdevices.com>
Co-authored-by: あく <alleteam@gmail.com>
Konstantin Volkov пре 3 година
родитељ
комит
492f147568

+ 56 - 0
.github/workflows/unit_tests.yml

@@ -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}}

+ 13 - 0
scripts/flipper/storage.py

@@ -340,6 +340,19 @@ class FlipperStorage:
         else:
             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):
         """Remove file or directory on Flipper"""
         self.send_and_wait_eol('storage remove "' + path + '"\r')

+ 16 - 0
scripts/storage.py

@@ -21,6 +21,11 @@ class Main(App):
         self.parser_mkdir.add_argument("flipper_path", help="Flipper path")
         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(
             "remove", help="Remove file/directory"
         )
@@ -275,6 +280,17 @@ class Main(App):
         storage.stop()
         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):
         self.logger.error("This test is wearing out flash memory.")
         self.logger.error("Never use it with internal storage(/int)")

+ 48 - 0
scripts/testing/await_flipper.py

@@ -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 - 0
scripts/testing/units.py

@@ -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()