From 3c470a8fea7fe2c6afee6459d38cc14da10d512c Mon Sep 17 00:00:00 2001 From: nearology Date: Tue, 10 Nov 2026 09:03:56 +0330 Subject: [PATCH] =?UTF-8?q?=D8=A7=D8=B6=D8=A7=D9=81=D9=87=20=D8=B4=D8=AF?= =?UTF-8?q?=D9=86=20=D9=88=D8=B1=DA=A9=20=D9=81=D9=84=D9=88=20=D8=AA=D8=B3?= =?UTF-8?q?=D8=AA=20=D8=B4=D8=A8=DA=A9=D9=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/software-test-aminpa-V1.5.yml | 30 ++++++++++ README.md | 39 +++++++++++++ scripts/software_test.py | 55 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 .gitea/workflows/software-test-aminpa-V1.5.yml create mode 100644 README.md create mode 100644 scripts/software_test.py diff --git a/.gitea/workflows/software-test-aminpa-V1.5.yml b/.gitea/workflows/software-test-aminpa-V1.5.yml new file mode 100644 index 0000000..f76350c --- /dev/null +++ b/.gitea/workflows/software-test-aminpa-V1.5.yml @@ -0,0 +1,30 @@ +name: Software test - Amin Pardaz Asia v1.5 + +on: + workflow_run: + workflows: + - flash-aminpa-V1.5.yml + types: + - completed + branches: + - main + workflow_dispatch: + +jobs: + ping-device: + if: ${{ gitea.event_name == 'workflow_dispatch' || gitea.event.workflow_run.conclusion == 'success' }} + runs-on: serial-runner + + steps: + - name: Checkout tested revision + uses: actions/checkout@v4 + with: + ref: ${{ gitea.event.workflow_run.head_sha || gitea.sha }} + + - name: Wait up to three minutes for device ping + env: + TEST_INTERFACE: ${{ vars.TEST_INTERFACE || 'eno1' }} + TEST_HOST_IP: ${{ vars.TEST_HOST_IP || '192.168.75.15' }} + TEST_DEVICE_IP: ${{ vars.TEST_DEVICE_IP }} + run: | + /usr/bin/python3 scripts/software_test.py --interface "$TEST_INTERFACE" --host-ip "$TEST_HOST_IP" --device-ip "$TEST_DEVICE_IP" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e20aed --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# فلش و تست دستگاه Amin Pardaz Asia v1.5 + +workflow اصلی `flash-aminpa-V1.5.yml` با push به شاخهٔ `main` اجرا می‌شود. +workflow تست نرم‌افزاری برای اجرا پس از موفقیت آن و همچنین اجرای دستی تعریف شده است. + +## متغیرهای Gitea برای تست نرم‌افزاری + +متغیرها را در تنظیمات مخزن Gitea، بخش **Actions → Variables** تعریف کنید. +نام متغیرها باید دقیقاً مطابق جدول باشد. + +| نام متغیر | کاربرد | پیش‌فرض | الزامی | +| --- | --- | --- | --- | +| `TEST_INTERFACE` | نام کارت شبکهٔ میزبان runner برای تست | `eno1` | خیر | +| `TEST_HOST_IP` | آدرس IPv4 میزبان runner روی کارت شبکهٔ تست | `192.168.75.15` | خیر | +| `TEST_DEVICE_IP` | آدرس IPv4 دستگاه فلش‌شده که باید به ping پاسخ دهد | ندارد | بله | + +`TEST_HOST_IP` آدرس میزبان است، نه دستگاه هدف. مقدار `TEST_DEVICE_IP` باید +با آن متفاوت باشد و با IP دستگاه پس از بوت سیستم‌عامل مطابقت داشته باشد؛ +این مقدار لزوماً همان `ipaddr` موقت در U-Boot نیست. + +IP میزبان باید از قبل روی کارت شبکهٔ انتخاب‌شده تنظیم شده باشد و مسیر رسیدن +به دستگاه از همان کارت شبکه عبور کند. اسکریپت تنظیمات شبکهٔ میزبان را تغییر نمی‌دهد. +مقادیر IP به‌صورت آدرس IPv4 وارد می‌شوند، نه رنج یا CIDR. + +اسکریپت با پایتون سیستم و ابزارهای سیستم `ip` و `ping` اجرا می‌شود. +پس از بررسی تنظیمات میزبان، حداکثر ۱۸۰ ثانیه منتظر پاسخ ping دستگاه می‌ماند؛ +اولین پاسخ موفق نتیجهٔ `PASS` دارد. نبود پاسخ تا پایان مهلت یا خطای تنظیمات +باعث `FAIL` و شکست workflow می‌شود. + +نمونهٔ اجرای دستی روی runner با جایگزین‌کردن IP واقعی دستگاه: + +```bash +/usr/bin/python3 scripts/software_test.py \ + --interface eno1 \ + --host-ip 192.168.75.15 \ + --device-ip +``` + +برای محل فایل‌های firmware و نقشهٔ فلش، [راهنمای firmware](firmware/README.md) را ببینید. diff --git a/scripts/software_test.py b/scripts/software_test.py new file mode 100644 index 0000000..caa675a --- /dev/null +++ b/scripts/software_test.py @@ -0,0 +1,55 @@ +"""Wait up to three minutes for the flashed device to answer ICMP.""" +import argparse +import ipaddress +import json +import subprocess +import sys + + +def test_device(interface, host_ip, device_ip): + host = str(ipaddress.IPv4Address(host_ip)) + device = str(ipaddress.IPv4Address(device_ip)) + if host == device: + raise ValueError('Device IP must differ from the test host IP') + if not interface or interface.startswith('-'): + raise ValueError('Invalid network interface') + result = subprocess.run( + ['ip', '-j', '-4', 'addr', 'show', 'dev', interface], + check=True, capture_output=True, text=True, timeout=5, + ) + addresses = json.loads(result.stdout) + if not any(info.get('local') == host for entry in addresses for info in entry.get('addr_info', [])): + raise ValueError(f'Host IP {host} is not configured on {interface}') + result = subprocess.run( + ['ip', '-j', '-4', 'route', 'get', device, 'from', host], + check=True, capture_output=True, text=True, timeout=5, + ) + routes = json.loads(result.stdout) + if not routes or any(route.get('dev') != interface for route in routes): + raise ValueError(f'Route to {device} does not use {interface}') + print(f'Waiting up to 180 seconds: {host} ({interface}) -> {device}', flush=True) + # -c 1 exits on the first reply; -w bounds the entire ping run. + result = subprocess.run( + ['ping', '-4', '-n', '-B', '-I', host, '-c', '1', '-i', '1', '-W', '1', '-w', '180', device], + timeout=180, + ) + if result.returncode: + raise RuntimeError(f'FAIL: {device} did not pass the ping test (exit {result.returncode})') + print(f'PASS: device {device} answered ping', flush=True) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--interface', default='eno1') + parser.add_argument('--host-ip', default='192.168.75.15') + parser.add_argument('--device-ip', required=True) + args = parser.parse_args() + test_device(args.interface, args.host_ip, args.device_ip) + + +if __name__ == '__main__': + try: + main() + except (ValueError, RuntimeError, OSError, subprocess.SubprocessError) as exc: + print(f'FAIL: {exc}', file=sys.stderr) + sys.exit(1)