From c18d0b73c1d9a5882eca6a33728b6ab1bc1dd0cf Mon Sep 17 00:00:00 2001 From: Pablo Saavedra Date: Mon, 18 Nov 2024 21:32:26 +0100 Subject: [PATCH] Add CI resources based on Robot-Framework * Include Docker setup and scripts (`Dockerfile`, `install-requirements.sh`, `podman-compose.sh`, `prepare-board.sh`). * Add initial Robot Framework resources, tests, and test setup files. * Integrate scripts for touch events and video performance testing with Robot Framework. * Add `conf/nginx.conf` for NGINX configuration and `docker-compose.yml` for environment setup. * Provide a comprehensive `README.md` for project setup and usage instructions. Also: * Add `.gitattributes` configuration for handling large files with Git LFS. * Include `.github/scripts/` with various sanitizer scripts (`run-all-sanatizers`, `sanatizer-pycodestyle`, `sanatizer-pyflake8`, `sanatizer-shellcheck`) for automated code quality checks. * Add GitHub Actions workflow (`sanatizers.yml`) to run sanitizers on pull requests. * Add `.gitignore` with entries for backup files, virtual environments, and testing artifacts. --- .ci/README.md | 63 ++++++++ .ci/conf/nginx.conf | 35 +++++ .ci/docker-compose.yml | 14 ++ .ci/docker/robot/Dockerfile | 5 + .ci/install-requirements.sh | 37 +++++ .ci/podman-compose.sh | 12 ++ .ci/prepare-board.sh | 30 ++++ .../html/bbb_sunflower_1080p_30fps_normal.mp4 | 3 + .ci/robot_framework/html/vertical_scroll.html | 101 +++++++++++++ .ci/robot_framework/html/video_fps.html | 64 ++++++++ .ci/robot_framework/images/pinch-gesture.png | Bin 0 -> 35740 bytes .ci/robot_framework/images/zoom-gesture.png | Bin 0 -> 34877 bytes .ci/robot_framework/init-robot.sh | 29 ++++ .ci/robot_framework/libs/TestUtils.py | 94 ++++++++++++ .ci/robot_framework/run-robot.sh | 34 +++++ .../tests/keywords_common.robot | 56 +++++++ .../tests/keywords_touch_events.robot | 66 +++++++++ .../tests/tests_000_common.robot | 12 ++ .../tests/tests_010_input_events.robot | 20 +++ .../tests/tests_015_video.robot | 42 ++++++ .../tests/tests_020_motionmark.robot | 49 +++++++ .ci/robot_framework/tests/variables.robot | 9 ++ .ci/run-tests.sh | 39 +++++ .ci/scripts/touch-one-finger-gesture.py | 76 ++++++++++ .ci/scripts/touch-two-fingers-gesture.py | 137 ++++++++++++++++++ .ci/setup-env-local.sh.sample | 10 ++ .ci/setup-env.sh | 35 +++++ .gitattributes | 1 + .github/scripts/run-all-sanatizers | 19 +++ .github/scripts/sanatizer-pycodestyle | 18 +++ .github/scripts/sanatizer-pyflake8 | 18 +++ .github/scripts/sanatizer-shellcheck | 24 +++ .github/workflows/sanatizers.yml | 19 +++ .gitignore | 12 ++ 34 files changed, 1183 insertions(+) create mode 100644 .ci/README.md create mode 100644 .ci/conf/nginx.conf create mode 100644 .ci/docker-compose.yml create mode 100644 .ci/docker/robot/Dockerfile create mode 100755 .ci/install-requirements.sh create mode 100755 .ci/podman-compose.sh create mode 100755 .ci/prepare-board.sh create mode 100644 .ci/robot_framework/html/bbb_sunflower_1080p_30fps_normal.mp4 create mode 100644 .ci/robot_framework/html/vertical_scroll.html create mode 100644 .ci/robot_framework/html/video_fps.html create mode 100644 .ci/robot_framework/images/pinch-gesture.png create mode 100644 .ci/robot_framework/images/zoom-gesture.png create mode 100755 .ci/robot_framework/init-robot.sh create mode 100755 .ci/robot_framework/libs/TestUtils.py create mode 100755 .ci/robot_framework/run-robot.sh create mode 100644 .ci/robot_framework/tests/keywords_common.robot create mode 100644 .ci/robot_framework/tests/keywords_touch_events.robot create mode 100644 .ci/robot_framework/tests/tests_000_common.robot create mode 100644 .ci/robot_framework/tests/tests_010_input_events.robot create mode 100644 .ci/robot_framework/tests/tests_015_video.robot create mode 100644 .ci/robot_framework/tests/tests_020_motionmark.robot create mode 100644 .ci/robot_framework/tests/variables.robot create mode 100755 .ci/run-tests.sh create mode 100755 .ci/scripts/touch-one-finger-gesture.py create mode 100755 .ci/scripts/touch-two-fingers-gesture.py create mode 100644 .ci/setup-env-local.sh.sample create mode 100755 .ci/setup-env.sh create mode 100644 .gitattributes create mode 100755 .github/scripts/run-all-sanatizers create mode 100755 .github/scripts/sanatizer-pycodestyle create mode 100755 .github/scripts/sanatizer-pyflake8 create mode 100755 .github/scripts/sanatizer-shellcheck create mode 100644 .github/workflows/sanatizers.yml diff --git a/.ci/README.md b/.ci/README.md new file mode 100644 index 0000000..a99bb8f --- /dev/null +++ b/.ci/README.md @@ -0,0 +1,63 @@ +A Robot Framework tests suite for automating the validation of the +meta-wpe-image images. + +# Installation + +We have the `install-requirements.sh` and `podman-compose.sh` scripts in the +project.The first one is a convenient script for installing the Podman +requirements. The second, is a wrapper for execute the podman-compose command +but with the environment variables defined in the setup-env.sh. + +``` sh +./install-requirements.sh +cp setup-env-local.sh.sample setup-env-local.sh # Use an editor for adapt the content +``` + +A sample environment setup file (`setup-env-local.sh.sample`) is provided to +guide the initial configuration. It sets the variables for the test board and +network configurations adapted to your environment. + +## How It Works + +To set up the testing environment, run: + +```sh +./podman-compose.sh up --force-recreate --always-recreate-deps --build -d -t 4 +``` + +Once the environment is running, you can trigger the tests with the +`./run-tests.sh` launcher: + +```sh +./run-tests.sh +``` +### Services Setup + +The `./podman-compose.sh up` command initializes the following services: + +- **webserver**: Runs an NGINX container, exposing port **8008** for HTTP + requests. +- **robot**: Runs a Python-based container configured for executing tests + using the Robot Framework. + +### Running Tests + +To execute the tests, use: + +```sh +./run-tests.sh [options] +``` + +Options: + +- `--force-recreate` : Recreate and build containers before running tests. +- `--help` : Display the help message for available options. + +### Stopping the Containers + +To stop the Podman containers, use: + +```sh +./podman-compose.sh down -t 4 +``` + diff --git a/.ci/conf/nginx.conf b/.ci/conf/nginx.conf new file mode 100644 index 0000000..1ffbdc8 --- /dev/null +++ b/.ci/conf/nginx.conf @@ -0,0 +1,35 @@ +server { + listen 8008; + listen [::]:8008; + server_name localhost; + + #access_log /var/log/nginx/host.access.log main; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + location /robot_framework/html/ { + root /; + autoindex on; + } + + location /tests_results/ { + root /; + autoindex on; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page + # /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} + + diff --git a/.ci/docker-compose.yml b/.ci/docker-compose.yml new file mode 100644 index 0000000..7bf1186 --- /dev/null +++ b/.ci/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3' +services: + webserver: + image: nginx:latest + network_mode: host + volumes: + - ./conf/nginx.conf:/etc/nginx/conf.d/default.conf + - ./robot_framework/html:/robot_framework/html + - ./tests_results:/tests_results + robot: + build: docker/robot/ + network_mode: host + volumes: + - .:/app diff --git a/.ci/docker/robot/Dockerfile b/.ci/docker/robot/Dockerfile new file mode 100644 index 0000000..c1f9cf4 --- /dev/null +++ b/.ci/docker/robot/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3-slim + +WORKDIR /app + +CMD ["robot_framework/init-robot.sh"] diff --git a/.ci/install-requirements.sh b/.ci/install-requirements.sh new file mode 100755 index 0000000..dc86c3e --- /dev/null +++ b/.ci/install-requirements.sh @@ -0,0 +1,37 @@ +#! /bin/sh -e + +# Identify the Linux distribution +if [ -f /etc/os-release ]; then + . /etc/os-release +else + echo "Distribution identification file /etc/os-release is missing." + exit 1 +fi + +# Function to install podman-compose on Fedora +install_fedora() { + echo "Installing podman-compose on Fedora..." + sudo yum install -y podman-compose pycodestyle python3-pyflakes shellcheck +} + +# Function to install podman-compose on Debian or Ubuntu +install_debian_ubuntu() { + echo "Installing podman-compose on $NAME..." + sudo apt update + sudo apt install -y podman-compose pycodestyle pyflakes3 shellcheck +} + +# Installation process based on the identified distribution +case $ID in + fedora) + install_fedora + ;; + ubuntu | debian) + install_debian_ubuntu + ;; + *) + echo "Your distribution ($ID) is not supported by this script." + exit 2 + ;; +esac + diff --git a/.ci/podman-compose.sh b/.ci/podman-compose.sh new file mode 100755 index 0000000..7e87b75 --- /dev/null +++ b/.ci/podman-compose.sh @@ -0,0 +1,12 @@ +#! /bin/sh + +set -e + +if [ ! -e ./setup-env.sh ] +then + echo "Please, create a ./setup-env.sh to run this command" + exit 1 +fi + +. ./setup-env.sh +exec podman-compose "$@" diff --git a/.ci/prepare-board.sh b/.ci/prepare-board.sh new file mode 100755 index 0000000..b4b7f2f --- /dev/null +++ b/.ci/prepare-board.sh @@ -0,0 +1,30 @@ +#! /bin/bash + +set -e + +BASEPATH="$(dirname "$(readlink -f "$0")")" + +SETUPENV="${BASEPATH}/setup-env.sh" + +if [ ! -e "${SETUPENV}" ] +then + echo "Please, create a ${SETUPENV} to run this command" + exit 1 +fi + +# shellcheck source=./setup-env.sh +. "${SETUPENV}" + +sshi() { + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "root@${TEST_BOARD_IP}" "$@" +} + +scpi() { + scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -r "$@" "root@${TEST_BOARD_IP}": +} + +pushd "${BASEPATH}" || exit 1 +scpi scripts +popd || exit 1 + +sshi "/usr/bin/kill-demo || true" diff --git a/.ci/robot_framework/html/bbb_sunflower_1080p_30fps_normal.mp4 b/.ci/robot_framework/html/bbb_sunflower_1080p_30fps_normal.mp4 new file mode 100644 index 0000000..b9acbba --- /dev/null +++ b/.ci/robot_framework/html/bbb_sunflower_1080p_30fps_normal.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c56cd32f709d1da3b93302381736b26ffb0da916b5bde950134b863a93ea5e7 +size 276134947 diff --git a/.ci/robot_framework/html/vertical_scroll.html b/.ci/robot_framework/html/vertical_scroll.html new file mode 100644 index 0000000..792f44e --- /dev/null +++ b/.ci/robot_framework/html/vertical_scroll.html @@ -0,0 +1,101 @@ + + + + + + Alternating Color Blocks + + + + + +
+ +
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
+ + + + diff --git a/.ci/robot_framework/html/video_fps.html b/.ci/robot_framework/html/video_fps.html new file mode 100644 index 0000000..4ccfc55 --- /dev/null +++ b/.ci/robot_framework/html/video_fps.html @@ -0,0 +1,64 @@ + + + + + + FPS Display Video + + + + + + + + +
FPS: 0
+ + + + + diff --git a/.ci/robot_framework/images/pinch-gesture.png b/.ci/robot_framework/images/pinch-gesture.png new file mode 100644 index 0000000000000000000000000000000000000000..65a819dff22612d0d67f8def7a180397cae40c50 GIT binary patch literal 35740 zcmeIbd0bOh_b-eVMeBgn0jok3Q*fZ5Rsor!Rm4<*Iv{0?T9L{WQ09aLixmqZ6;xCv zD=12YD99L+Kq>GbLPQ7=5CQ}U1VVriGAHjoIrO>rxu5rsclg}jJ6z6(nl#ya?S0l- zdkx>U&(7~ZIyqSh^A z;D5BQeUCo73|#nSmu_ikZPh{_J$yDQbE3B;1sP3Xi(QVMz3i}ISJRf_OHD>B|H8nv zB@@rS{ndEuc)$CEQ`xqfBQ<-L*XjJab?fNGFP2W!J^rrWAx^y%fnT%DV1sSjTVLHW z*1m5q948+>fy~&t&0^=Bs)IbEYnJ2N5W9?GU%bEM8ea0a*Oi)eH;Ni}?6SjU6+y4D zVKe*I#Y!);FTe!Bb%=XkAk&85YgHf??}6Xe+*uR12!0!JLu)1c?)7%<^YFVkgKb*# z1SXa(+6aGm-f>wC{7%c}do745;QFMuKyrbXS_?{~wXk}$7R-j0*8fNB|$WQ ztG(HZ5li7{Tdr-)><+XA8G)awEoJ4%D|6(NIa@%B5OU)1)k-|e1o(>@rH2k1OAuyA zJIS{1^p_H4UE*0Y485cRubitNmJ3YHwof@CVrRWk7|^w!pz#WOl~ZuE>=If5P0r>R89Cc3SQYU(89dgF?|f?QR^ zzuagu_gnrJuoZX3!ZWDpwHf{LT}0yg>%@U^3T3ub6I> z_PUsJ1Z;UXOZ9m`{G6^9k+DWoi0p6Jk_Jl)#&XD$m1RIq@bwpoPyeNBh$N0XVz4~1 zg}Rra#;ek?%5y3{LcJAgKRI9uhKMUK1PlTFYI^arZH^fdTzV5bL)FKkJ)nzn1>8tf z>^(Amc(=IN7&MxSSY88O4CdM#5yM0o0@e3nK^G`y~JFG+2$snFn1hqCkQa7cU8Z$onoU~MTb`mYyLpP-PK_3SE8O3iF z&~xb0?EEHkz-h9rrv; z6lSpT?d?lIbJ_|G&sWPNoD`uQwOQT9ZI;5#_a>BtgQSmv++b|=&mAGxl1_^PzX1>8&=>&!X_RfE`sKD&bchcw5w zQbgg`oAU9+Qe4-2bJhzPiKSfUiW=@y6 zCuRgt8(OaPr}LpX1nR=y&erN6E)mV8y=*zP<&8X%9>B+64f;wFNfd}r^B+MRiwiQ= zsK5su1pbd{hZ!67AO=~fmQW%NELP8$Vc$uXOWJvHnDpt*8tA2Q{ zR-96`5~Vp@va%l&MZzfs>LHepZZB(;<}=dR>N52)z|@+ueTy^#Bq|4Vhf|(6kySi( z__+8%i&0BhwKOtRM!p9Avm}-a+f6g-3j-#6AD|u^t3_;Om%JK^Nc*$lWg+?*HTUzQ zZ=`&b#FRMI30vni zM@^>_KW_;QmkRFDhkdEN%Tf7dx(3xPUGJ%MU+xT~1+%i$7L3FuoA`o#UF6xEdOu$(TC zry|*6Y8|o(LBthDD<`_R(mHTkJxN30kAzYB(_~C?aHlCQoJUve!PVuiR29-y-c{d& zk@~}7u9U8>X(x&p?Pkx-@v0UzCtX-bl%=$rmq^nUW8>Azc0slmk~qLtTvyba7{%apn1bxZV~$GnSuYjJB6glmw7b zQ(vH7M|-P1caL8vh^dfLHr+mLaQpM(O<+Q0pZh5G;NG?RPTHVOq^_e{Gtb`x?7eJ_ zw#o96_ob*MDRfmg=>X{XJKaqTiK6Tbusq;3&hpxU2A`$fgTF~|`J((ZWWBMf@AF#l z%_n;(Z#v|7Um4#b2lBe~Xy#o|hM<~Yce2>4P&o)oH+01>fXE$>#!rw)75hFG<@Dgv z(ft?6Iai4fi_3|XDyfw(*}09hYgqzd|r@{!ChpY;n3YR zHX`&bIi9-C6)W_Z5S=+oy+>mAb!4WTp^NwMCx-amYXFrHzu-l{8hr@$B@>1tJKueSLf5%cZ$*ll(F`{EH7FEShF6W?womfIFc@EAVz& z#m>|&TAJ&XY3rgMt)==v8D$6^c1~yi8!wSpo~P~4Z_JR$8vQTsQASBq|<2ujii`|fR~S)KRTPZTO_?LX>Z{+U%rdnSMa)h&I`!p2L_R@q zp}VW8hq4vSuD?~IL{B*He3b75uYMLZzlxs<>aL$HZG}vFl;u1(Dv1x1@>9;NI(9|> zzgN;G|G&f!wD7K!$D3#emwT-`R%sQTt!j(MQGt;M;g!bNkG_Z?JV5S@2`{MKf&O^X zE-HUtT~NZrolef?dJ*KC*OW7ddjE1LS-R$T1<;aV1?6z zMLU5A?6dUaAe-;G1^3{&qVWrrds~uL9(fqtZcSFD#;rEqr^@cL-5dMT-E{_f z-X@I&WI-!xD)=F+DOc5d^j#47n{IzJ%MQ9zp5)%$g=|iWPG#N@e7i_e5OLrD{@Pa+ zn{HnXov~Hs>WE_?*IT2UcV~QsDf;v*?`-_3)I1>UOhfFTU^Ce+E6?4|5p)7(p$PWI zCa~uyYWUQA$TQE|p$F$qv__u}92B(J0TqhlZ`ZKtY!~WNHMX?@{a`2gbD6YtoK=Tx zW_FuTYFs*k0na)7c2H}>l~(iZd|4$m@{zXLoNT8!VJA+WKAw{Tg?dyEsA1fBm-Q%N(%QQ}bTLWUzppc*=X`rJQ>X43>*OYTt3OIH6?enr2XWetV3{k?l&=RT~uV3uv~d|=} zCFlqGLL_N9+jH@hE8pK=GJ*^_uyJ^BC|c&IJ1K%JW}Mu2@jqTQHE)y&Oau}QgniJi z?{BuH56&805_1Q1dS<=RyGLUTongD>o%|;Kdf9PGVwt#D6_%R!>2G!hynY>FHWd?J zN3%1N(@f9!R0avNSmXrs15znw-lAs1a~|K=lbPI}i8gt%`jp6m zT9u0A38-=6G%dKLZ@*vcU~3a9reYXYg?i zdydj;)>xOT8@GG5~0X@2M(-ME5!i4v4EZ z{cQe4(A|jjK>}ap7!v*Fce>^L`h|^vull@@c5t(a`}69nJtH<`jBc8#TL8U6LnPMr z3ylOj_m&;poPUsh`|UV)uFvYSe6MBih4303qyHg!{=F9)zlS` zxu1Lw=RidhOtPZYdXDzqdKvq<_esonCipfkvJU3PeR)D6Sy&T9$s{vmprE$j)b``7u&|7OVeffDner56r_2n21y)5cbGy z)LJ4tia&;I;T`-7bmE;4`;Q1MT;s%z_`ACMnq}?i)QfA0*!lGf8-aJK+L7367S6>d zqle2{B7NwF#_mO(gizQ)MJ?4xu<++V(BvB7>nkh3b+W66?xn_O7Q5w- zGBsFg;KBKrvLOvJS>Y40kHXi`oG1@zod+Sc72Jfmu}iFb zZF)8ocn|(y*GLrQ;j=8K;_8Pc?oN`vuj0u5ZfEK&O|Fw_cj%ii+EFnfWESYG_1PTG zMwqP(Fb^^?5#OMJuv^Baja9Vi?soKJ=CvE36SH=-TjN_IG-4?22Y%(rI?{z7DJVpIR3&SjWeiQ4w*e(8)nR9kNakk%w3`wE_GapiA<$Qxq^vMFt zITZ-|aXZIJ5z8sfPuox8E(4tk;@e7L--6V*fez+I+}FP(y(RsEGaq*`K_@yXsFm>g zg^j>2xlV?-rOT>m-|+n&3wE?J=HaDE4|zRIg!6R`7#zx6K$8}!Z=N@*Tx^zB11#OJ zYL72geN`z8edm+))NKXS@bxfE)Zfpf%f|MdLef;q0bblyw;jYB`DvyL0j8GqLV&5k z1Qr6!g#dFQz+4D0{}&7}zq7>rp```y1vtMDY%TaXwS7rdNk+ID99)tgS8-IO(ft6x))-+Z}$CC`7+ z7Xgb_d{H%JvZr77%_r=D4SU7ZqMNY+x5j^a(2;sC_-(((iom;Pb6)T+v|b7i_w_xN zXFjv%5z{2xwKMFuY5Ah$7q{NW^jVsb)KS^`079P()xds73hO@c@R>a6zC3SGyi0TL zKS#-UYI(eE?AvhwM~}|7^JbSo@JdMIa;W&SsUSQ1S?vCI!|iEVTwmfJ#i--qd(p9b zo1Q*?mTljIr@gq0PCeJv<>ko__V6{S=S7kWvQygC{b?QTB|W%pfKq?MZ3wISrQ&Hj zkrUZ@0Ti9S7+}5h)5n%V;81_>2MtjEqk~|F(2;&Tde2EaTy?Y$*R?Tqj9$`s)m}4U@RtNMxPY;8zr~PMRqqC7$!eu*kZ=zFq7gKp%?M@OL zzgs`;Ea}>f=J*Tm>IPaLgAO&lNG)hKFZg>RzWz{ln*4nawS)c;c_rvCHYosdijgT= z5ZL@@;^e>x7x3!R-HQQ%!sKzW4tfKr+q?*>lzg^pg{g#Z^vp^*V~4rRO( zwxpxoFN2}aG(~^hWs?y=>UAPblOQ&3hKlM7EP7KFc08M z*H+kWyiACAJUJRg7677J6X~nZgDl}@fjJ`$S?G)c+(w`FK6%;>SGAl-_Lg5v^$))j z0z?#T_1PV&l~iPC*MOH*r3MXVUv!`2x&A^)m+6DPfcxQ2;G6zQJy<|(cAphqSt)6z zrvsCNXrW5g)~1WD^7;cn$>J!D8tiXX>FS?J!q5V4^B)nh!2&1i>p-g#`?fmdm%jp- z^wV}1QoBAxt;WCvW`9h~gSoaH=`Jo2+3Cu|?^Iiv^+-`YGRglzj4V(nefsztw1=lu-sQ9lBUtbom|ooG@1U~!lYvBMbu!yM0jfORz~pCpS2H1) zI4<&tjz(_lsk!eFN+ZAz(Zuvfv|YyYZ0F*K7-RQv3C&om?;N@6COKkpx=!7DhI6}t zfk0#c^;Uj_Ra-P1otnoS%eUQVj4nt*7x=kY4hTvEFegtJw(6Pb-rT91U<2!38mxO| zb)S3>_548nGDv({eK9q!6mWlD@fM#4gm3PuO326oyh?RQEPT^qS;R&l-Cz0BD~!Rg zAz~{{x#Z8z3xVPG&LaHRABzCQk2VPbTGh5WZ*=y72$b={WMI8O)U+x%un=8c7$5C7 z4bO}zwT;oIZN^`NV*T~0@s5(D@@}{&Itt6}Y2-a=E+)@*Up!yB{mJ*?VNRyLwHOdEVa0RQ z6pDUy5Qhk_0bUg!e{-Nr+8~n0Ba>YD`YdR{O&V${%=KHL0Ax2Te8Ii?+4 zO4%6`zb>nFe#$V^y>yfOD~$6POayRK-TIh~#I__O+H9r|ge1@64D zk`^vdQY6sImQv>AbJvK|uL$A-p43XIYmc_GqfqrBQxyDu8MNc#=5A}HB`D=V<)<}- zteA-DbDBD2-ztL|&}x zm*L9qP&Q7HVb!ci&`WFX1gzT3bS^OA`8j~wNXQyx+x6a{vx;K(qD!qEfq{5#yJhl~ z#^^c0y4St>=k!HEyG6U+_MT8Cp;J2n_p4yVTQ+Zd41)`uskx!0U;V#8=wZ3Vsvn0eX7REGm6>|)EL_){&Z$im{1%Chd>x9^-2nF1eIJ4C*^ z*5qZrU%B{uaKkmpUu$-fggE4@8w9WFpo;xpo$EL#orCXB(VEt|1Lf8@=c zFb)e33XYp?xA7b>$xzo?@7=zpWn)GFNu56AG>eR z-R2b!_8IGY>slCqS4~Fsou5WFvjn=SrsqmAUK;z0%Y;qL_?|LrM1BlyU3C4vkYv1uvWgcj)kaceGY|%Sdi~`1(AGJI(vy54Hi1^MZnBsP8Pqo{4$?m zkz-!qo|@+xVZj37!MQH<8(A-@fXkUhNsyx1i8&ul4QlW9CqO=%ibiMIlB{s326adh z&g-i8)a*!zvYhwKZ8q-|ZRV#9SDs-QVzXH#S9oK{B&lzzYl0wPaet7gAVA`80({DQ z#l-&JwrL+WAgcArd|tIN*(PX@EW*3^3#pJbikS;rtMZ}D0mkyS3;+?$gjAGhbk7ow zSNv^mdoO)k*g^UbVJ4h!fhu0mRAjWBV~`?RSC&ygt)r`8wDCC&SO z&CZ-n{ss-_xSX?$^lg0E=e~@v#pT_->!^nlow4p}sg*;1&bfSZh+3SDGb}{WtSLYD zlvz&pgsr(e{+{wMF!Q`5^aiG>Xtrxa2$VGS?Yq{IZvV*kT{b(<(Q&RKe5Oh#Coem@ zw{Hb>$Cj71p$AX4yOPyV*?t+_*^UxLg34o{JPLc}T``W2P%;O=4_+6iMHpk@ zWS4bmFa2aOZz|SFtGdu9d=-YT4SdR@oUG?I)_vnPptF|z8LZm8W+5=)?JmHBrvhtq zmXRI&7dwDOdzj~uwH+9UGlr`pJ2ghn7}mX@gg4D`W%py&cTYZ3)g#dXfcxIC;?4Vv zFkMTO8<&GjTIcz52G)y?I$rz}lMo8~{)M1(A?REPIv0Y@g`jgG==`4+bZ*o{b^l!q z--f7OvltcA*Tmsw*;)$%C)0+;b(%|vbV5b7s+~M>li~m((z`r-_44cG{}F2agN|6C zEx2>`6260KW^BM1Y6Ositm@5x@>GOJQvJp0B1;fn)bwQ7OEWTa6#J9998`RB!=ZfL0whM8J{~8w zRx#8aExCRUslJv|VzrPA5`vK_32sa|iRx}vf!=Y--F+eu4QW+mL(%VyJ{@1SoJf}s z*s2u>uf81$r&OX!*G$-}n_l=AD)z_Wd|lFi#=sy!HpI(pQPLEbG$dR8;(g?~Kl%1} zF1S1KsYgg@6A}MdC&&IjlmDlrMP)YoGZ4G#&g7}NPfH+y9I=J)Ue6ehjX=ZbSlb89|I7%w?w8v}9#peP0m&%|| zk8-nRW(PbW*v!zP#CB+xd)EiFZU|t$?VE> zfFn`YWyRkU#140M?q=oZBKmiT{o%Dm>?N6bAtnsc*yRydET)Xm2Ue$!VGC zFHeu2>H_=-30ICPol}Q6Op)##wr3~b%<%{PEPdToJ}~a#3eUnl1)~~i#4zxuFRP^3 z2zxA;kF7nH%T_Sel;t?g$iV$W61I|OOO&J>lh&eCl4oozTfT?A1z%AD0fzm-@Ot_Q zH+2D|;SlQPN0jB#TOsad$Wy8c@g3$fl%x`56Kg}Wz&`j}N;HDiYx}rVICb?dLfvwO zps>uB)|Lyny6Pls_3I4~6UyP_V?-jcGBGicGUFYkV3sam46}UoPNF^(=rk>58eFN!h*0xto1vXiBwA8zPvHq*`^DWn*+HBrT)x8^9OdLXHvkoWCVC;`C? zJ222c;N(DJA5J<=RO~0J+|=8entx@}b5i6E_LC-Tira`<(qgJ>t}kszD8mU;E(CMp zY@JK+&w7t-;4o z91}tO)Fhs~57vd6r2c~_Ov@3xs_cVSL`XTD@?W+iNQYRuLXu9(44J3^+!GVUBμ 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 literal 0 HcmV?d00001 diff --git a/.ci/robot_framework/init-robot.sh b/.ci/robot_framework/init-robot.sh new file mode 100755 index 0000000..c4e4161 --- /dev/null +++ b/.ci/robot_framework/init-robot.sh @@ -0,0 +1,29 @@ +#! /bin/bash + +set -e + +apt-get update -y +apt-get install -y git ssh \ + build-essential \ + imagemagick tesseract-ocr ghostscript libdmtx0b libzbar0 + +pip install virtualenv + +virtualenv .venv_robot_framework +. ./.venv_robot_framework/bin/activate + +# Install Robot depends +pip install gitpython pygithub \ + robotframework \ + robotframework-doctestlibrary \ + robotframework-retryfailed \ + robotframework-seleniumlibrary \ + robotframework-sshlibrary + +pushd robot_framework/html/ +if [ ! -d "rbyers" ]; then + git clone https://github.com/RByers/rbyers.github.io.git rbyers +fi +popd + +tail -f /dev/null diff --git a/.ci/robot_framework/libs/TestUtils.py b/.ci/robot_framework/libs/TestUtils.py new file mode 100755 index 0000000..849527e --- /dev/null +++ b/.ci/robot_framework/libs/TestUtils.py @@ -0,0 +1,94 @@ +import argparse +import os +import paramiko +import subprocess +import sys +import time +from multiprocessing import Process +from selenium import webdriver + + +class TestUtils: + """Robot Framework library for interacting with remote hosts and running + tests via SSH and WebDriver.""" + + def __init__(self): + self.driver = None + + def print_envvar(starts_with=""): + test_args_env_vars = {key: value for key, value in os.environ.items() if + key.startswith(starts_with)} + + # Sort the dictionary by key + sorted_test_args_env_vars = dict(sorted(test_args_env_vars.items())) + + # Print the variables in a well-tabulated format + for key, value in sorted_test_args_env_vars.items(): + print(f"{key:<30} '{value}'") + + def ssh_command(self, ip, command, quiet=False, debug=False): + """Run SSH command.""" + if debug: + print(f"COMMAND: {ip} {command}", file=sys.stderr) + username = 'root' + password = None + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + client.connect(ip, username=username, password=None) + except paramiko.ssh_exception.SSHException as e: + if not password: + client.get_transport().auth_none(username) + else: + raise e + stdin, stdout, stderr = client.exec_command(command) + + output = stdout.read().decode('utf-8').strip() + error = stderr.read().decode('utf-8').strip() + + # Ensure command completes before closing the session + stdout.channel.recv_exit_status() + + client.close() + + return output, error + + def ssh_command_in_background(self, ip, command): + """Run SSH command in the background without closing the SSH + connection.""" + process = Process(target=self.ssh_command, args=(ip, command)) + process.start() + + def ssh_force_kill(self, ip, text): + """Force kill all related process.""" + print(f"RUN: Killing all '{text}' related processes ...") + command = f"pgrep {text} && pgrep {text} | xargs kill -9 || echo '{text} not running'" + return self.ssh_command(ip, command, quiet=True) + + def ssh_reboot_force_reboot(self, ip): + command("(echo b > /proc/sysrq-trigger) /dev/null &") + self.ssh_command_in_background(ip, command) + + def ssh_reboot_wait_for_reboot(self, ip, timeout=60): + start_time = time.time() + while True: + try: + self.ssh_command(ip, "true") + print("Host is back online.") + break + except Exception: + print("Host is still down...") + if time.time() - start_time > timeout: + print("Timeout reached, stopping wait.") + break + time.sleep(5) + + def ssh_webdriver_remote_start(self, ip, port): + command = ( + f"WPEWebDriver --host={ip} " + f"--port={port} --host-all" + ) + self.ssh_command_in_background(ip, command) + + def ssh_webdriver_remote_stop(self, ip): + self.ssh_force_kill(ip, "WPEWebDriver") diff --git a/.ci/robot_framework/run-robot.sh b/.ci/robot_framework/run-robot.sh new file mode 100755 index 0000000..93e59ef --- /dev/null +++ b/.ci/robot_framework/run-robot.sh @@ -0,0 +1,34 @@ +#! /bin/bash + +set -e + +. ./.venv_robot_framework/bin/activate + +SETUPENV="./setup-env.sh" + +if [ ! -e "${SETUPENV}" ] +then + echo "${SETUPENV} not found in the current path (${PWD})" + exit 1 +fi + +# shellcheck source=./setup-env.sh +. ${SETUPENV} + +DATE=$(date +%Y%m%d_%H%M%S) +mkdir -p "tests_results/${DATE}_robot_${TESTS_RESULTS}/" + +rm -rf tests_results/robot +ln -s "${DATE}_robot_${TESTS_RESULTS}" tests_results/robot +cd "tests_results/${DATE}_robot_${TESTS_RESULTS}/" + +# Copy the setup-env files. +cp ../../setup-env*sh . + +exec robot --name "WPE image tests" \ + --consolewidth 158 \ + --exclude skip \ + --skiponfailure ignoreonfail \ + --listener RetryFailed:2 \ + ../../robot_framework/tests/tests_*.robot + diff --git a/.ci/robot_framework/tests/keywords_common.robot b/.ci/robot_framework/tests/keywords_common.robot new file mode 100644 index 0000000..b1d3ab9 --- /dev/null +++ b/.ci/robot_framework/tests/keywords_common.robot @@ -0,0 +1,56 @@ +*** Settings *** +Library Collections +Library OperatingSystem +Library ../libs/TestUtils.py + +*** Keywords *** +Create WPEWebKitOptions + [Arguments] ${binary_name} ${binary_path} @{other_params} + + ${wpe_options} = Evaluate sys.modules['selenium.webdriver'].WPEWebKitOptions() sys, selenium.webdriver + ${wpe_options.binary_location} Set Variable ${binary_path} + FOR ${param} IN @{other_params} + Call Method ${wpe_options} add_argument ${param} + END + Call Method ${wpe_options} set_capability browserName ${binary_name} + RETURN ${wpe_options} + +Get Remote CPU Load + ${TEST_BOARD_IP} Get Environment Variable TEST_BOARD_IP + ${stdout}= SSH Command ${TEST_BOARD_IP} uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' + ${value}= Evaluate float(${stdout}[0]) + RETURN ${value} + +Get Remote Memory Used + ${TEST_BOARD_IP} Get Environment Variable TEST_BOARD_IP + ${stdout}= SSH Command ${TEST_BOARD_IP} free -m | grep Mem | awk '{print $3}' + ${value}= Evaluate float(${stdout}[0]) + RETURN ${value} + +Prepare Board + ${rc} ${output}= Run And Return Rc And Output ${PREPARE_BOARD_SCRIPT} + Should Be Equal As Integers ${rc} 0 msg=Prepare Board command failed with non-zero exit status + Log output: ${output} + +Webdriver Remote Start + ${TEST_BOARD_IP} Get Environment Variable TEST_BOARD_IP + ${TEST_BOARD_WEBDRIVER_PORT} Get Environment Variable TEST_BOARD_WEBDRIVER_PORT + + # Force kill previous launchers + SSH Webdriver Remote Stop ${TEST_BOARD_IP} + SSH Force Kill ${TEST_BOARD_IP} cog + + SSH Webdriver Remote Start ${TEST_BOARD_IP} ${TEST_BOARD_WEBDRIVER_PORT} + Sleep 5 + + ${wpe_options} = Create WPEWebKitOptions cog /usr/bin/cog-fdo-exported-wayland --maximized --automation + Create Webdriver Remote command_executor=${TEST_BOARD_IP}:${TEST_BOARD_WEBDRIVER_PORT} options=${wpe_options} + +Webdriver Remote Stop + ${TEST_BOARD_IP} Get Environment Variable TEST_BOARD_IP + ${TEST_BOARD_WEBDRIVER_PORT} Get Environment Variable TEST_BOARD_WEBDRIVER_PORT + + Close All Browsers + SSH Webdriver Remote Stop ${TEST_BOARD_IP} + SSH Force Kill ${TEST_BOARD_IP} cog + diff --git a/.ci/robot_framework/tests/keywords_touch_events.robot b/.ci/robot_framework/tests/keywords_touch_events.robot new file mode 100644 index 0000000..1c8e01a --- /dev/null +++ b/.ci/robot_framework/tests/keywords_touch_events.robot @@ -0,0 +1,66 @@ +*** Variables *** +${SCROLL_POSITION} 300 +${SWIPE_POSITION} 1000 +${SWIPE_THRESHOLD} 150 +${SWIPE_WAIT} 5 + +${BASELINE_IMAGES_PATH} /app/robot_framework/images/ +${PINCH_GESTURE_IMAGE} pinch-gesture.png +${ZOOM_GESTURE_IMAGE} zoom-gesture.png + +*** Settings *** +Library DocTest.VisualTest +Library OperatingSystem +Library ../libs/TestUtils.py + +Resource variables.robot +Resource keywords_common.robot + +*** Keywords *** +Check Browser Touch Events Using Uinput + ${TEST_BOARD_IP} Get Environment Variable TEST_BOARD_IP + ${TEST_WEBSERVER_IP} Get Environment Variable TEST_WEBSERVER_IP + ${TEST_WEBSERVER_PORT} Get Environment Variable TEST_WEBSERVER_PORT + ${PAGE} Set Variable http://${TEST_WEBSERVER_IP}:${TEST_WEBSERVER_PORT}/robot_framework/html/vertical_scroll.html + ${PAGE2} Set Variable http://${TEST_WEBSERVER_IP}:${TEST_WEBSERVER_PORT}/robot_framework/html/rbyers/paint.html + + Go to ${PAGE} + Capture Page Screenshot + + # Scroll + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-one-finger-gesture.py --duration 5 --steps 40 --delay-on-touch-up 0 100 500 100 200 + Capture Page Screenshot + ${scroll_position}= Execute JavaScript return window.pageYOffset; + Should Be Equal As Numbers ${scroll_position} ${SCROLL_POSITION} + + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-one-finger-gesture.py --duration 5 --steps 40 --delay-on-touch-up 0 100 200 100 500 + Capture Page Screenshot + ${scroll_position}= Execute JavaScript return window.pageYOffset; + Should Be Equal As Numbers ${scroll_position} 0 + + # Swipe + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-one-finger-gesture.py --duration 0.1 --steps 40 --delay-on-touch-up 0 100 500 100 200 + Capture Page Screenshot + Sleep ${SWIPE_WAIT} + ${scroll_position}= Execute JavaScript return window.pageYOffset; + ${swipe_upper_position}= Set Variable ${SWIPE_POSITION} + ${SWIPE_THRESHOLD} + Should Be True ${scroll_position} > ${SWIPE_POSITION} and ${scroll_position} < ${swipe_upper_position} + + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-one-finger-gesture.py --duration 0.1 --steps 40 --delay-on-touch-up 0 100 200 100 500 + Capture Page Screenshot + Sleep ${SWIPE_WAIT} + ${scroll_position}= Execute JavaScript return window.pageYOffset; + Should Be True ${scroll_position} >= 0 and ${scroll_position} < ${SWIPE_THRESHOLD} + + # Multitouch: Pinch + Go to ${PAGE2} + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-two-fingers-gesture.py --duration 2 --steps 40 900 200 900 500 900 800 900 500 + Capture Page Screenshot ${PINCH_GESTURE_IMAGE} + Compare Images ${BASELINE_IMAGES_PATH}/${PINCH_GESTURE_IMAGE} ${PINCH_GESTURE_IMAGE} + + # Multitouch: Zoom + Go to ${PAGE2} + SSH Command ${TEST_BOARD_IP} /root/scripts/touch-two-fingers-gesture.py --duration 2 --steps 40 900 500 900 200 900 500 900 800 + Capture Page Screenshot ${ZOOM_GESTURE_IMAGE} + Compare Images ${BASELINE_IMAGES_PATH}/${ZOOM_GESTURE_IMAGE} ${ZOOM_GESTURE_IMAGE} + diff --git a/.ci/robot_framework/tests/tests_000_common.robot b/.ci/robot_framework/tests/tests_000_common.robot new file mode 100644 index 0000000..5da0826 --- /dev/null +++ b/.ci/robot_framework/tests/tests_000_common.robot @@ -0,0 +1,12 @@ +*** Settings *** +Test Timeout 600 seconds + +Resource variables.robot +Resource keywords_common.robot + +*** Test Cases *** +Setup + ${TEST_BOARD_SETUP_SKIP} Get Environment Variable TEST_BOARD_SETUP_SKIP default=no + Run Keyword If '${TEST_BOARD_SETUP_SKIP}' != 'yes' + ... Prepare Board + diff --git a/.ci/robot_framework/tests/tests_010_input_events.robot b/.ci/robot_framework/tests/tests_010_input_events.robot new file mode 100644 index 0000000..5bb70bc --- /dev/null +++ b/.ci/robot_framework/tests/tests_010_input_events.robot @@ -0,0 +1,20 @@ +*** Settings *** +Test Timeout 60 seconds + +Suite Setup Webdriver Remote Start +Suite Teardown Webdriver Remote Stop + +Library Collections +Library DocTest.VisualTest +Library OperatingSystem +Library SeleniumLibrary +Library ../libs/TestUtils.py + +Resource variables.robot +Resource keywords_touch_events.robot + +*** Test Cases *** +Test Check Browser Touch Events Using Uinput + [Tags] ignoreonfail + Check Browser Touch Events Using Uinput + diff --git a/.ci/robot_framework/tests/tests_015_video.robot b/.ci/robot_framework/tests/tests_015_video.robot new file mode 100644 index 0000000..fb910d9 --- /dev/null +++ b/.ci/robot_framework/tests/tests_015_video.robot @@ -0,0 +1,42 @@ +*** Settings *** + +Test Timeout 60 seconds + +Suite Setup Webdriver Remote Start +Suite Teardown Webdriver Remote Stop + +Library SeleniumLibrary + +Resource variables.robot +Resource keywords_common.robot + +*** Keywords *** +Get FPS Value + ${fps_text}= Get Text id=fps + ${fps}= Convert To Number ${fps_text.split(":")[1].strip()} + RETURN ${fps} + +*** Test Cases *** +Verify Full HD 30 FPS + ${TEST_WEBSERVER_IP} Get Environment Variable TEST_WEBSERVER_IP + ${TEST_WEBSERVER_PORT} Get Environment Variable TEST_WEBSERVER_PORT + ${PAGE} Set Variable http://${TEST_WEBSERVER_IP}:${TEST_WEBSERVER_PORT}/robot_framework/html/video_fps.html + + Go to ${PAGE} + Sleep 20 seconds + + ${memory_used}= Get Remote Memory Used + Log Memory used: ${memory_used} + + ${cpu_load}= Get Remote CPU Load + Log CPU load: ${cpu_load} + + ${fps} Get FPS Value + Log FPS value: ${fps} + + Capture Page Screenshot + + Should Be True ${fps} > ${VIDEO_30_FPS_THRESHOLD_FPS} + Should Be True ${cpu_load} < ${VIDEO_30_FPS_THRESHOLD_CPU_LOAD} + Should Be True ${memory_used} < ${VIDEO_30_FPS_THRESHOLD_MEMORY_USED} + diff --git a/.ci/robot_framework/tests/tests_020_motionmark.robot b/.ci/robot_framework/tests/tests_020_motionmark.robot new file mode 100644 index 0000000..f6fe9de --- /dev/null +++ b/.ci/robot_framework/tests/tests_020_motionmark.robot @@ -0,0 +1,49 @@ +*** Variables *** +${URL} https://browserbench.org/MotionMark1.2/ +${RUN_BENCHMARK_BUTTON} xpath=//*[@id="intro"]/div[2]/button +${SCORE_SELECTOR} xpath=//*[@id="results"]/div[2]/div[1]/div[1] +${TEST_AGAIN_BUTTON} xpath=//button[contains(@onclick, 'benchmarkController.startBenchmark()') and contains(text(), 'Test Again')] + +*** Settings *** + +Test Timeout 900 seconds + +Suite Setup Webdriver Remote Start +Suite Teardown Webdriver Remote Stop + +Library SeleniumLibrary + +Resource variables.robot +Resource keywords_common.robot + +*** Keywords *** +Capture Images Until Test Completion + [Documentation] Captures a screenshot each time a new test section loads until the "Test Again" button appears. + + ${index}= Set Variable 1 + WHILE "True" + Sleep 20s + Capture Page Screenshot motionmark_test_${index}.png + ${index}= Evaluate ${index} + 1 + + Run Keyword And Ignore Error Element Should Be Visible ${TEST_AGAIN_BUTTON} + ${is_test_again_visible}= Run Keyword And Return Status Element Should Be Visible ${TEST_AGAIN_BUTTON} + Run Keyword If ${is_test_again_visible} Exit For Loop + END + +*** Test Cases *** +Run MotionMark Benchmark And Validate Score + [Documentation] Loads MotionMark benchmark, runs it, waits for the score, and validates. + + Go to ${URL} + Wait Until Page Contains Element ${RUN_BENCHMARK_BUTTON} + Click Element ${RUN_BENCHMARK_BUTTON} + + Capture Images Until Test Completion + + Wait Until Page Contains Element ${TEST_AGAIN_BUTTON} timeout=600s + Capture Page Screenshot + ${score}= Get Text ${SCORE_SELECTOR} + Log MotionMark Score : ${score} + Should Be True ${score} > ${MOTIONMARK_MIN_SCORE} + diff --git a/.ci/robot_framework/tests/variables.robot b/.ci/robot_framework/tests/variables.robot new file mode 100644 index 0000000..7677d47 --- /dev/null +++ b/.ci/robot_framework/tests/variables.robot @@ -0,0 +1,9 @@ +*** Variables *** +${PREPARE_BOARD_SCRIPT} ../../prepare-board.sh + +${MOTIONMARK_MIN_SCORE} 90 + +${VIDEO_30_FPS_THRESHOLD_CPU_LOAD} 3 +${VIDEO_30_FPS_THRESHOLD_MEMORY_USED} 850 +${VIDEO_30_FPS_THRESHOLD_FPS} 28 + diff --git a/.ci/run-tests.sh b/.ci/run-tests.sh new file mode 100755 index 0000000..35a577c --- /dev/null +++ b/.ci/run-tests.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Function to display help +show_help() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --force-recreate Recreate and build containers before running tests" + echo " --help Show this help message" +} + +# Check arguments +force_recreate=false + +for arg in "$@"; do + case $arg in + --force-recreate) + force_recreate=true + ;; + --help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $arg" + show_help + exit 1 + ;; + esac +done + +# Run podman-compose only if --force-recreate is specified +if [ "$force_recreate" = true ]; then + ./podman-compose.sh up --force-recreate --always-recreate-deps --build -d -t 4 > /dev/null 2>&1 +fi + +# Run the test script +podman exec -ti meta-wpe-image-tests_robot_1 ./robot_framework/run-robot.sh + + diff --git a/.ci/scripts/touch-one-finger-gesture.py b/.ci/scripts/touch-one-finger-gesture.py new file mode 100755 index 0000000..8a818e9 --- /dev/null +++ b/.ci/scripts/touch-one-finger-gesture.py @@ -0,0 +1,76 @@ +#! /usr/bin/python3 + +import argparse +import uinput +import time + +DEVICE_READY_TIMEOUT = 3 +DISPLAY_WIDTH = 1920 +DISPLAY_HEIGHT = 1080 + + +def simulate_scroll(device, start_x, start_y, end_x, end_y, + duration=0.5, steps=10, delay_on_touch_up=0): + """Simulate a swipe gesture from (start_x, start_y) to (end_x, end_y)""" + + # Calculate step increments for smooth motion + if steps > 0: + step_x = (end_x - start_x) // steps + step_y = (end_y - start_y) // steps + delay = 1.0 * duration / steps # Time delay between each step + + # Start touch + device.emit(uinput.ABS_X, start_x, syn=False) + device.emit(uinput.ABS_Y, start_y, syn=False) + device.emit(uinput.BTN_TOUCH, 1) # Press touch + device.syn() + + # Move in steps to simulate dragging + for i in range(steps): + device.emit(uinput.ABS_X, start_x + step_x * i, syn=False) + device.emit(uinput.ABS_Y, start_y + step_y * i, syn=False) + device.syn() + time.sleep(delay) + + # End touch + time.sleep(delay_on_touch_up) + device.emit(uinput.ABS_X, end_x, syn=False) + device.emit(uinput.ABS_Y, end_y, syn=False) + device.emit(uinput.BTN_TOUCH, 0) # Release touch + device.syn() + + +def main(): + parser = argparse.ArgumentParser(description="Simulate a swipe gesture.") + parser.add_argument("start_x", type=int, + help="Starting X position of the swipe.") + parser.add_argument("start_y", type=int, + help="Starting Y position of the swipe.") + parser.add_argument("end_x", type=int, + help="Ending X position of the swipe.") + parser.add_argument("end_y", type=int, + help="Ending Y position of the swipe.") + parser.add_argument("--duration", type=float, default=0.5, + help="Duration of the swipe in seconds.") + parser.add_argument("--steps", type=int, default=10, + help="Number of steps for the swipe.") + parser.add_argument("--delay-on-touch-up", type=float, default=0, + help="Delay on touch up.") + + args = parser.parse_args() + + # Create a device that can emit touch events + device = uinput.Device([ + uinput.ABS_X + (0, DISPLAY_WIDTH, 0, 0), + uinput.ABS_Y + (0, DISPLAY_HEIGHT, 0, 0), + uinput.BTN_TOUCH, + ]) + + time.sleep(DEVICE_READY_TIMEOUT) + + simulate_scroll(device, args.start_x, args.start_y, args.end_x, args.end_y, + args.duration, args.steps, args.delay_on_touch_up) + + +if __name__ == "__main__": + main() diff --git a/.ci/scripts/touch-two-fingers-gesture.py b/.ci/scripts/touch-two-fingers-gesture.py new file mode 100755 index 0000000..2f07720 --- /dev/null +++ b/.ci/scripts/touch-two-fingers-gesture.py @@ -0,0 +1,137 @@ +#!/usr/bin/python3 + +import argparse +import uinput +import time + +DEVICE_READY_TIMEOUT = 3 +DISPLAY_WIDTH = 1920 +DISPLAY_HEIGHT = 1080 + + +def simulate_two_finger_gesture(device, + start_x1, start_y1, end_x1, end_y1, + start_x2, start_y2, end_x2, end_y2, + duration=0.5, steps=10): + """ Simulate a two-finger scroll from (start_x1, start_y1) and + (start_x2, start_y2) to (end_x1, end_y1) and (end_x2, end_y2) + """ + + tracking_id1 = 1 + tracking_id2 = 2 + + # Calculate step increments for smooth motion + step_x1 = (end_x1 - start_x1) // steps + step_y1 = (end_y1 - start_y1) // steps + step_x2 = (end_x2 - start_x2) // steps + step_y2 = (end_y2 - start_y2) // steps + delay = duration / steps # Time delay between each step + + # Start touch for both fingers + device.emit(uinput.ABS_MT_TRACKING_ID, tracking_id1, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, start_x1, syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, start_y1, syn=False) + device.emit(uinput.ABS_MT_SLOT, 1, syn=False) + + device.emit(uinput.ABS_MT_TRACKING_ID, tracking_id2, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, start_x2, syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, start_y2, syn=False) + device.emit(uinput.ABS_MT_SLOT, 0, syn=False) + + device.emit(uinput.BTN_TOUCH, 1) # Press touch for first finger + device.emit(uinput.ABS_X, start_x1, syn=False) + device.emit(uinput.ABS_Y, start_y1, syn=False) + device.syn() + + # Move both fingers in steps to simulate dragging + for i in range(steps): + # Update first finger position + device.emit(uinput.ABS_MT_SLOT, 1, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, start_x1 + step_x1 * i, + syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, start_y1 + step_y1 * i, + syn=False) + device.emit(uinput.ABS_X, start_x1 + step_x1 * i, syn=False) + device.emit(uinput.ABS_Y, start_y1 + step_y1 * i, syn=False) + + # Update second finger position + device.emit(uinput.ABS_MT_SLOT, 0, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, start_x2 + step_x2 * i, + syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, start_y2 + step_y2 * i, + syn=False) + + device.syn() + + # Wait between steps to simulate smooth scroll + time.sleep(delay) + + # End touch for both fingers + device.emit(uinput.ABS_MT_SLOT, 1, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, end_x1, syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, end_y1, syn=False) + device.emit(uinput.ABS_X, end_x1, syn=False) + device.emit(uinput.ABS_Y, end_y1, syn=False) + device.emit(uinput.ABS_MT_TRACKING_ID, -1, syn=False) + device.emit(uinput.BTN_TOUCH, 0) # Release touch for first finger + device.syn() + + device.emit(uinput.ABS_MT_SLOT, 0, syn=False) + device.emit(uinput.ABS_MT_POSITION_X, end_x2, syn=False) + device.emit(uinput.ABS_MT_POSITION_Y, end_y2, syn=False) + device.emit(uinput.ABS_X, end_x2, syn=False) + device.emit(uinput.ABS_Y, end_y2, syn=False) + device.emit(uinput.ABS_MT_TRACKING_ID, -1, syn=False) + device.emit(uinput.BTN_TOUCH, 0) # Release touch for second finger + device.syn() + + +def main(): + parser = argparse.ArgumentParser(description="Simulate a swipe gesture.") + parser.add_argument("start_x1", type=int, + help="Starting X1 position of the swipe.") + parser.add_argument("start_y1", type=int, + help="Starting Y1 position of the swipe.") + parser.add_argument("end_x1", type=int, + help="Ending X1 position of the swipe.") + parser.add_argument("end_y1", type=int, + help="Ending Y1 position of the swipe.") + parser.add_argument("start_x2", type=int, + help="Starting X2 position of the swipe.") + parser.add_argument("start_y2", type=int, + help="Starting Y2 position of the swipe.") + parser.add_argument("end_x2", type=int, + help="Ending X2 position of the swipe.") + parser.add_argument("end_y2", type=int, + help="Ending Y2 position of the swipe.") + parser.add_argument("--duration", type=float, default=0.5, + help="Duration of the swipe in seconds.") + parser.add_argument("--steps", type=int, default=10, + help="Number of steps for the swipe.") + + args = parser.parse_args() + + # Create a device that can emit touch events + device = uinput.Device([ + uinput.ABS_MT_TRACKING_ID + (0, 65535, 0, 0), + uinput.ABS_MT_POSITION_X + (0, DISPLAY_WIDTH, 0, 0), + uinput.ABS_MT_POSITION_Y + (0, DISPLAY_HEIGHT, 0, 0), + uinput.ABS_X + (0, DISPLAY_WIDTH, 0, 0), + uinput.ABS_Y + (0, DISPLAY_HEIGHT, 0, 0), + uinput.ABS_MT_SLOT + (0, 10, 0, 0), + uinput.BTN_TOUCH, + ]) + + time.sleep(DEVICE_READY_TIMEOUT) + + # Simulate a two-finger scroll gesture + simulate_two_finger_gesture(device, + args.start_x1, args.start_y1, + args.end_x1, args.end_y1, + args.start_x2, args.start_y2, + args.end_x2, args.end_y2, + args.duration, args.steps) + + +if __name__ == "__main__": + main() diff --git a/.ci/setup-env-local.sh.sample b/.ci/setup-env-local.sh.sample new file mode 100644 index 0000000..e45a33f --- /dev/null +++ b/.ci/setup-env-local.sh.sample @@ -0,0 +1,10 @@ +#!/bin/sh + +# export TEST_BOARD_SETUP_SKIP="yes" + +# export TEST_BOARD_IP="192.168.1.105" +# export TEST_BOARD_NAME="rpi5" + +# export TEST_WEBSERVER_IP="192.168.1.92" +# export TEST_WEBSERVER_PORT="8008" + diff --git a/.ci/setup-env.sh b/.ci/setup-env.sh new file mode 100755 index 0000000..0d020fa --- /dev/null +++ b/.ci/setup-env.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +################################################################################ +# Environment variables definitions +################################################################################# + +# export TEST_BOARD_SETUP_SKIP="yes" + +export TEST_BOARD_WEBDRIVER_PORT="8888" + +export TEST_BOARD_IP="192.168.1.105" +export TEST_BOARD_NAME="rpi5" + +export TEST_WEBSERVER_IP="192.168.1.92" +export TEST_WEBSERVER_PORT="8008" + +################################################################################ +# Load local setup +################################################################################# + +# XXX: Get the basepath from the environment +SETUPENVLOCAL="setup-env-local.sh" +APPBASEPATH="/app" +APPSETUPENVLOCAL="${APPBASEPATH}/${SETUPENVLOCAL}" + +if [ -f "${APPSETUPENVLOCAL}" ]; then + # shellcheck source=./setup-env.sh + . "${APPSETUPENVLOCAL}" +elif [ -f "${SETUPENVLOCAL}" ]; then + # shellcheck source=./setup-env.sh + . "./${SETUPENVLOCAL}" +else + echo "WARNING: Not ${APPSETUPENVLOCAL} nor ${SETUPENVLOCAL} found" +fi + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b90b5b9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.ci/robot_framework/html/bbb_sunflower_1080p_30fps_normal.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/scripts/run-all-sanatizers b/.github/scripts/run-all-sanatizers new file mode 100755 index 0000000..2bb1897 --- /dev/null +++ b/.github/scripts/run-all-sanatizers @@ -0,0 +1,19 @@ +#!/bin/bash + +# Navigate to the directory containing the sanitizer scripts +SANATIZERS_DIR="$(dirname $(realpath $0))" + +# Initialize an exit status variable +exit_status=0 + +# Loop through each sanitizer script and execute it +for script in $SANATIZERS_DIR/sanatizer-*; do + "$script" + # Check the exit status of the script + if [ $? -ne 0 ]; then + echo "Error: $script failed" + exit_status=1 + fi +done + +exit $exit_status diff --git a/.github/scripts/sanatizer-pycodestyle b/.github/scripts/sanatizer-pycodestyle new file mode 100755 index 0000000..76402e2 --- /dev/null +++ b/.github/scripts/sanatizer-pycodestyle @@ -0,0 +1,18 @@ +#! /bin/bash + +set -e + +scripts=$(find . -executable -type f ! -path '*/\.*' -exec grep -lE '^#! *(|/usr/bin/env +|/bin/|/usr/bin/)(python|python3)' {} +) +echo "Running pycodestyle on the following scripts:" +echo "$scripts" +errors=0 +for script in $scripts; do + if ! pycodestyle "$script"; then + errors=$((errors + 1)) + fi +done +if [ "$errors" -ne 0 ]; then + echo "pycodestyle found issues." + exit 1 +fi +echo "pycodestyle passed successfully." diff --git a/.github/scripts/sanatizer-pyflake8 b/.github/scripts/sanatizer-pyflake8 new file mode 100755 index 0000000..de8f859 --- /dev/null +++ b/.github/scripts/sanatizer-pyflake8 @@ -0,0 +1,18 @@ +#! /bin/bash + +set -e + +scripts=$(find . -executable -type f ! -path '*/\.*' -exec grep -lE '^#! *(|/usr/bin/env +|/bin/|/usr/bin/)(python|python3)' {} +) +echo "Running pyflakes3 on the following scripts:" +echo "$scripts" +errors=0 +for script in $scripts; do + if ! pyflakes3 "$script"; then + errors=$((errors + 1)) + fi +done +if [ "$errors" -ne 0 ]; then + echo "pyflakes3 found issues." + exit 1 +fi +echo "pyflakes3 passed successfully." diff --git a/.github/scripts/sanatizer-shellcheck b/.github/scripts/sanatizer-shellcheck new file mode 100755 index 0000000..65b11b9 --- /dev/null +++ b/.github/scripts/sanatizer-shellcheck @@ -0,0 +1,24 @@ +#! /bin/bash + +set -e + +scripts=$(find . -executable -type f ! -path '*/\.*' -exec grep -lE '^#! *(|/usr/bin/env +|/bin/|/usr/bin/)(sh|bash|dash)' {} +) +echo "Running ShellCheck on the following scripts:" +echo "$scripts" +errors=0 + +# Force the creation of the setup-env.sh and so +touch ./setup-env.sh +mkdir -p ./.venv_robot_framework/bin +touch ./.venv_robot_framework/bin/activate + +for script in $scripts; do + if ! shellcheck -x "$script"; then + errors=$((errors + 1)) + fi +done +if [ "$errors" -ne 0 ]; then + echo "ShellCheck found issues." + exit 1 +fi +echo "ShellCheck passed successfully." diff --git a/.github/workflows/sanatizers.yml b/.github/workflows/sanatizers.yml new file mode 100644 index 0000000..e0936ee --- /dev/null +++ b/.github/workflows/sanatizers.yml @@ -0,0 +1,19 @@ +name: Sanatizers + +on: + pull_request: + types: [synchronize, opened, reopened] + +jobs: + sanatizers: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install requirements + run: sudo apt-get install python3-flake8 python3-pycodestyle shellcheck + + - name: Run sanatizers + run: ./.github/scripts/run-all-sanatizers + diff --git a/.gitignore b/.gitignore index cb055f9..4f804ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,19 @@ __pycache__ +*.bak *.pyc *.pyo *.swp +*.tmp *.orig *.rej *~ + +# Python venv +venv* +.env +.virtualenv +.venv_robot_framework + +.ci/robot_framework/html/rbyers +.ci/setup-env-local.sh +.ci/tests_results/