Snap for 11367679 from 56d3b7e5a2f7954e93ea241a45d24b49247933a6 to 24Q2-release

Change-Id: Iaae21a1693e9f25cc28087b0b0bf4a6131f9b856
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 6e763e5..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-# Build, test and check the code against the linter and clippy
-name: Build, Test, Format and Clippy
-
-on:
-  push:
-    branches: [ main ]
-  pull_request:
-    branches: [ main ]
-
-env:
-  CARGO_TERM_COLOR: always
-
-jobs:
-  build_and_test:
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [macos-latest, ubuntu-latest, windows-latest]
-    steps:
-    - uses: actions/checkout@v3
-    - name: Install Rust 1.67.1
-      uses: actions-rs/toolchain@v1
-      with:
-        toolchain: 1.67.1
-        override: true
-        components: rustfmt, clippy
-    - name: Build
-      run: cargo build
-    - name: Test
-      run: cargo test
-    - name: Fmt
-      run: cargo fmt --check --quiet
-    - name: Clippy
-      run: cargo clippy --no-deps -- --deny warnings
diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
new file mode 100644
index 0000000..7a19021
--- /dev/null
+++ b/.github/workflows/build_and_test.yml
@@ -0,0 +1,44 @@
+name: Build, Check, Test
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+env:
+  CARGO_TERM_COLOR: always
+  PY_COLORS: "1"
+
+jobs:
+  build_and_test:
+    name: Build, Check, Test
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Install Rust 1.67.1
+      uses: actions-rs/toolchain@v1
+      with:
+        toolchain: 1.67.1
+        override: true
+        components: rustfmt, clippy
+    - name: Set Up Python 3.11
+      uses: actions/setup-python@v4
+      with:
+        python-version: 3.11
+    - name: Install
+      run: |
+        pip install --upgrade pip
+        pip install ./py/pica/
+        pip install pytest=="7.4.4"
+        pip install pytest_asyncio=="0.23.3"
+    - name: Build
+      run: cargo build
+    - name: Test
+      run: cargo test
+    - name: Fmt
+      run: cargo fmt --check --quiet
+    - name: Clippy
+      run: cargo clippy --no-deps -- --deny warnings
+    - name: Run Python tests suite
+      run: pytest --log-cli-level=DEBUG -v
diff --git a/.github/workflows/python.yml b/.github/workflows/python_format.yml
similarity index 80%
rename from .github/workflows/python.yml
rename to .github/workflows/python_format.yml
index 839c84b..1e551db 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python_format.yml
@@ -8,7 +8,7 @@
 
 jobs:
   format:
-    name: Check Python formatting
+    name: Check format
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
@@ -20,4 +20,4 @@
         run: |
           pip install --upgrade pip
           pip install black=="23.12.1"
-      - run: black --check scripts/ --exclude pica
+      - run: black --check tests/ py/pica --exclude py/pica/pica/packets
diff --git a/.gitignore b/.gitignore
index e762de7..c80aebd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 target/
 __pycache__
+build/
+*.egg-info/
+artifacts
diff --git a/Cargo.lock b/Cargo.lock
index b065f75..2fd8ab3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,21 @@
 version = 3
 
 [[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
 name = "anyhow"
 version = "1.0.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -46,6 +61,21 @@
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
 name = "bitflags"
 version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -67,6 +97,15 @@
 checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -201,6 +240,12 @@
 ]
 
 [[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
 name = "glam"
 version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -277,7 +322,7 @@
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2",
+ "socket2 0.4.9",
  "tokio",
  "tower-service",
  "tracing",
@@ -298,9 +343,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.139"
+version = "0.2.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
 
 [[package]]
 name = "log"
@@ -318,26 +363,34 @@
 checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
 
 [[package]]
-name = "mio"
-version = "0.8.6"
+name = "miniz_oxide"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
 dependencies = [
  "libc",
- "log",
  "wasi",
- "windows-sys 0.45.0",
+ "windows-sys",
 ]
 
 [[package]]
 name = "num-derive"
-version = "0.4.1"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.38",
+ "syn 1.0.89",
 ]
 
 [[package]]
@@ -360,6 +413,15 @@
 ]
 
 [[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "once_cell"
 version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -447,7 +509,7 @@
 
 [[package]]
 name = "pica"
-version = "0.1.3"
+version = "0.1.7"
 dependencies = [
  "anyhow",
  "bytes",
@@ -468,9 +530,9 @@
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.8"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
 
 [[package]]
 name = "pin-utils"
@@ -531,6 +593,12 @@
 ]
 
 [[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
 name = "ryu"
 version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -589,6 +657,16 @@
 ]
 
 [[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
 name = "syn"
 version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -641,26 +719,26 @@
 
 [[package]]
 name = "tokio"
-version = "1.28.0"
+version = "1.35.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
 dependencies = [
- "autocfg",
+ "backtrace",
  "bytes",
  "libc",
  "mio",
  "num_cpus",
  "pin-project-lite",
- "socket2",
+ "socket2 0.5.5",
  "tokio-macros",
- "windows-sys 0.48.0",
+ "windows-sys",
 ]
 
 [[package]]
 name = "tokio-macros"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -810,132 +888,66 @@
 
 [[package]]
 name = "windows-sys"
-version = "0.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
-dependencies = [
- "windows-targets 0.42.1",
-]
-
-[[package]]
-name = "windows-sys"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-targets 0.48.5",
+ "windows-targets",
 ]
 
 [[package]]
 name = "windows-targets"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
 dependencies = [
- "windows_aarch64_gnullvm 0.42.1",
- "windows_aarch64_msvc 0.42.1",
- "windows_i686_gnu 0.42.1",
- "windows_i686_msvc 0.42.1",
- "windows_x86_64_gnu 0.42.1",
- "windows_x86_64_gnullvm 0.42.1",
- "windows_x86_64_msvc 0.42.1",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.42.1"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
diff --git a/Cargo.toml b/Cargo.toml
index 86009de..2fc7bf7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pica"
-version = "0.1.3"
+version = "0.1.7"
 edition = "2021"
 description = "Pica is a virtual UWB Controller implementing the FiRa UCI specification."
 repository = "https://github.com/google/pica"
@@ -39,13 +39,13 @@
 pdl-compiler = "0.2.3"
 
 [dependencies]
-tokio = { version = "1.25.0", features = [ "fs", "io-util", "macros", "net", "rt" ] }
+tokio = { version = "1.32.0", features = [ "fs", "io-util", "macros", "net", "rt" ] }
 tokio-stream = { version = "0.1.8", features = ["sync"] }
 bytes = "1"
 anyhow = "1.0.56"
-num-derive = "0.4.1"
+num-derive = "0.3.3"
 num-traits = "0.2.17"
-pdl-runtime = "0.2.3"
+pdl-runtime = "0.2.2"
 thiserror = "1.0.49"
 glam = "0.23.0"
 hyper = { version = "0.14", features = ["server", "stream", "http1", "tcp"], optional = true }
diff --git a/README.md b/README.md
index bb0db30..f53cb62 100644
--- a/README.md
+++ b/README.md
@@ -117,3 +117,23 @@
 
 Pica also implements HTTP commands, the documentation is available at `http://0.0.0.0:3000/openapi`.
 The set of HTTP commands let the user interact with Pica amd modify its scene.
+
+# Tests
+
+Setup your python env:
+
+```bash
+python3 -m venv venv
+source venv/bin/activate
+pip install pytest
+pip install pytest_asyncio
+pip install -e py/pica/
+```
+
+Then run the tests
+
+```bash
+pytest --log-cli-level=DEBUG -v
+```
+
+The tests are located in `./tests/`
diff --git a/scripts/console.py b/py/pica/console.py
similarity index 100%
rename from scripts/console.py
rename to py/pica/console.py
diff --git a/scripts/pica/__init__.py b/py/pica/pica/__init__.py
similarity index 93%
rename from scripts/pica/__init__.py
rename to py/pica/pica/__init__.py
index 39e105c..ba2b155 100644
--- a/scripts/pica/__init__.py
+++ b/py/pica/pica/__init__.py
@@ -1,8 +1,8 @@
-
 import asyncio
 from typing import Union
 from .packets import uci
 
+
 class Host:
     def __init__(self, reader, writer, mac_address: bytes):
         self.reader = reader
@@ -14,17 +14,17 @@
         loop = asyncio.get_event_loop()
         self.reader_task = loop.create_task(self._read_packets())
 
-
     @staticmethod
-    async def connect(address: str, port: int, mac_address: bytes) -> 'Host':
+    async def connect(address: str, port: int, mac_address: bytes) -> "Host":
         reader, writer = await asyncio.open_connection(address, port)
         return Host(reader, writer, mac_address)
 
     def disconnect(self):
         self.writer.close()
+        self.reader_task.cancel()
 
     async def _read_exact(self, expected_len: int) -> bytes:
-        """ Read an exact number of bytes from the socket.
+        """Read an exact number of bytes from the socket.
 
         Raises an exception if the socket gets disconnected."""
         received = bytes()
@@ -34,7 +34,7 @@
         return received
 
     async def _read_packet(self) -> bytes:
-        """ Read a single UCI packet from the socket.
+        """Read a single UCI packet from the socket.
 
         The packet is automatically re-assembled if segmented on
         the UCI transport."""
@@ -63,7 +63,7 @@
                     pass
 
     async def _read_packets(self):
-        """ Loop reading UCI packets from the socket.
+        """Loop reading UCI packets from the socket.
         Receiving packets are added to the control queue."""
         try:
             while True:
@@ -84,7 +84,7 @@
     def send_data(self, packet: uci.DataPacket):
         packet = bytearray(packet.serialize())
         size = len(packet) - 4
-        size_bytes = size.to_bytes(2, byteorder='little')
+        size_bytes = size.to_bytes(2, byteorder="little")
         packet[2] = size_bytes[0]
         packet[3] = size_bytes[1]
         self.writer.write(packet)
diff --git a/scripts/pica/packets/__init__.py b/py/pica/pica/packets/__init__.py
similarity index 100%
rename from scripts/pica/packets/__init__.py
rename to py/pica/pica/packets/__init__.py
diff --git a/scripts/pica/packets/uci.py b/py/pica/pica/packets/uci.py
similarity index 100%
rename from scripts/pica/packets/uci.py
rename to py/pica/pica/packets/uci.py
diff --git a/py/pica/pyproject.toml b/py/pica/pyproject.toml
new file mode 100644
index 0000000..3f7acac
--- /dev/null
+++ b/py/pica/pyproject.toml
@@ -0,0 +1,5 @@
+[project]
+name = "pica"
+dynamic = ["version"]
+description = "UCI host helpers"
+requires-python = ">=3.10"
diff --git a/scripts/pica/packets/__init__.py b/tests/__init__.py
similarity index 100%
copy from scripts/pica/packets/__init__.py
copy to tests/__init__.py
diff --git a/scripts/data_transfer_example.py b/tests/data_transfer.py
similarity index 92%
rename from scripts/data_transfer_example.py
rename to tests/data_transfer.py
index 37f0b74..f7d731c 100755
--- a/scripts/data_transfer_example.py
+++ b/tests/data_transfer.py
@@ -18,12 +18,13 @@
 import argparse
 from pica import Host
 from pica.packets import uci
-from helper import init
+from .helper import init
+from pathlib import Path
 
 MAX_DATA_PACKET_PAYLOAD_SIZE = 1024
 
 
-async def data_message_send(host: Host, peer: Host, file: str):
+async def data_message_send(host: Host, peer: Host, file: Path):
     await init(host)
 
     host.send_control(
@@ -96,7 +97,9 @@
     await host.expect_control(uci.SessionDeinitRsp(status=uci.StatusCode.UCI_STATUS_OK))
 
 
-async def data_transfer(host: Host, dst_mac_address: bytes, file: str, session_id: int):
+async def data_transfer(
+    host: Host, dst_mac_address: bytes, file: Path, session_id: int
+):
     try:
         with open(file, "rb") as f:
             b = f.read()
@@ -158,7 +161,7 @@
         print(e)
 
 
-async def run(address: str, uci_port: int, http_port: int, file: str):
+async def run(address: str, uci_port: int, file: Path):
     try:
         host0 = await Host.connect(address, uci_port, bytes([0, 1]))
         host1 = await Host.connect(address, uci_port, bytes([0, 2]))
@@ -191,10 +194,7 @@
         "--uci-port", type=int, default=7000, help="Select the pica TCP UCI port"
     )
     parser.add_argument(
-        "--http-port", type=int, default=3000, help="Select the pica HTTP port"
-    )
-    parser.add_argument(
-        "--file", type=str, required=True, help="Select the file to transfer"
+        "--file", type=Path, required=True, help="Select the file to transfer"
     )
     asyncio.run(run(**vars(parser.parse_args())))
 
diff --git a/scripts/helper.py b/tests/helper.py
similarity index 100%
rename from scripts/helper.py
rename to tests/helper.py
diff --git a/scripts/ranging_example.py b/tests/ranging.py
similarity index 96%
rename from scripts/ranging_example.py
rename to tests/ranging.py
index 071528a..7af8224 100755
--- a/scripts/ranging_example.py
+++ b/tests/ranging.py
@@ -16,10 +16,11 @@
 
 import asyncio
 import argparse
+import logging
 
 from pica import Host
 from pica.packets import uci
-from helper import init
+from .helper import init
 
 
 async def controller(host: Host, peer: Host):
@@ -234,12 +235,12 @@
     await host.expect_control(uci.SessionDeinitRsp(status=uci.StatusCode.UCI_STATUS_OK))
 
 
-async def run(address: str, uci_port: int, http_port: int):
+async def run(address: str, uci_port: int):
     try:
         host0 = await Host.connect(address, uci_port, bytes([0, 1]))
         host1 = await Host.connect(address, uci_port, bytes([0, 2]))
     except Exception:
-        print(
+        logging.debug(
             f"Failed to connect to Pica server at address {address}:{uci_port}\n"
             + "Make sure the server is running"
         )
@@ -252,7 +253,7 @@
     host0.disconnect()
     host1.disconnect()
 
-    print("Ranging test completed")
+    logging.debug("Ranging test completed")
 
 
 def main():
@@ -267,9 +268,6 @@
     parser.add_argument(
         "--uci-port", type=int, default=7000, help="Select the pica TCP UCI port"
     )
-    parser.add_argument(
-        "--http-port", type=int, default=3000, help="Select the pica HTTP port"
-    )
     asyncio.run(run(**vars(parser.parse_args())))
 
 
diff --git a/tests/test_runner.py b/tests/test_runner.py
new file mode 100644
index 0000000..aa7a709
--- /dev/null
+++ b/tests/test_runner.py
@@ -0,0 +1,66 @@
+import asyncio
+from asyncio.subprocess import Process
+import pytest
+import pytest_asyncio
+import logging
+import os
+
+from datetime import datetime
+from pathlib import Path
+from typing import Tuple
+
+from . import ranging, data_transfer
+
+PICA_BIN = Path("./target/debug/pica-server")
+DATA_FILE = Path("README.md")
+PICA_LOCALHOST = "127.0.0.1"
+
+logging.basicConfig(level=os.environ.get("PICA_LOGLEVEL", "DEBUG").upper())
+
+
+def setup_artifacts(test_name: str) -> Tuple[Path, Path]:
+    artifacts = Path("./artifacts")
+    artifacts.mkdir(parents=True, exist_ok=True)
+
+    current_dt = datetime.now()
+    formatted_date = current_dt.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]
+
+    f1 = artifacts / f"{formatted_date}_pica_{test_name}_stdout.txt"
+    f1.touch(exist_ok=True)
+
+    f2 = artifacts / f"{formatted_date}_pica_{test_name}_stderr.txt"
+    f2.touch(exist_ok=True)
+
+    return (f1, f2)
+
+
+@pytest_asyncio.fixture
+async def pica_port(request, unused_tcp_port):
+    (stdout, stderr) = setup_artifacts(request.node.name)
+    if not PICA_BIN.exists():
+        raise FileNotFoundError(f"{PICA_BIN} not found")
+
+    with stdout.open("w") as fstdout, stderr.open("w") as fstderr:
+        process = await asyncio.create_subprocess_exec(
+            PICA_BIN,
+            "--uci-port",
+            str(unused_tcp_port),
+            stdout=fstdout,
+            stderr=fstderr,
+        )
+        await asyncio.sleep(100 / 1000)  # Let pica boot up
+
+        yield unused_tcp_port
+
+        process.terminate()
+        await process.wait()
+
+
[email protected]
+async def test_ranging(pica_port):
+    await ranging.run(PICA_LOCALHOST, pica_port)
+
+
[email protected]
+async def test_data_transfer(pica_port):
+    await data_transfer.run(PICA_LOCALHOST, pica_port, DATA_FILE)