From 4414dda60a8eae7c6bb9ececd9ae3873d079261a Mon Sep 17 00:00:00 2001 From: netanelC Date: Fri, 1 May 2026 13:32:32 +0300 Subject: [PATCH 1/6] feat: add testing unified api Co-authored-by: Copilot --- Ammeters/base_ammeter.py | 2 ++ config/config.yaml | 18 +++++++++--------- src/testing/test_framework.py | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Ammeters/base_ammeter.py b/Ammeters/base_ammeter.py index d9db1e6..1159663 100644 --- a/Ammeters/base_ammeter.py +++ b/Ammeters/base_ammeter.py @@ -16,6 +16,8 @@ def start_server(self): The server will run indefinitely, handling one client request at a time. """ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + # Allow the socket to be reused immediately after the program exits + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('localhost', self.port)) s.listen() print(f"{self.__class__.__name__} is running on port {self.port}") diff --git a/config/config.yaml b/config/config.yaml index 198dfc3..bef2ddc 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,15 +5,15 @@ testing: sampling_frequency_hz: NULL ammeters: -# greenlee: -# port: 5000 -# command: "MEASURE_GREENLEE -get_measurement" -# entes: -# port: 5001 -# command: "MEASURE_ENTES -get_data" -# circutor: -# port: 5002 -# command: "MEASURE_CIRCUTOR -get_measurement" + greenlee: + port: 5000 + command: "MEASURE_GREENLEE -get_measurement" + entes: + port: 5001 + command: "MEASURE_ENTES -get_data" + circutor: + port: 5002 + command: "MEASURE_CIRCUTOR -get_measurement" analysis: statistical_metrics: diff --git a/src/testing/test_framework.py b/src/testing/test_framework.py index 9bee874..6d5b202 100644 --- a/src/testing/test_framework.py +++ b/src/testing/test_framework.py @@ -1,11 +1,26 @@ +from typing import Dict, Optional from ..utils.config import load_config +from Ammeters.client import request_current_from_ammeter class AmmeterTestFramework: def __init__(self, config_path: str = "config/config.yaml"): self.config = load_config(config_path) + def get_single_reading(self, ammeter_type: str) -> Optional[float]: + """ + Fetches a single current reading from the specified ammeter. + """ + if ammeter_type not in self.config.get('ammeters', {}): + raise ValueError(f"Unknown ammeter type: {ammeter_type}") + + ammeter_config = self.config['ammeters'][ammeter_type] + port = ammeter_config['port'] + command = ammeter_config['command'].encode('utf-8') + + request_current_from_ammeter(port, command) + def run_test(self, ammeter_type: str) -> Dict: pass \ No newline at end of file From b686e9eb39538343cba7ea6c3c5b7f4c538a28d5 Mon Sep 17 00:00:00 2001 From: netanelC Date: Mon, 4 May 2026 22:05:34 +0300 Subject: [PATCH 2/6] feat: strict the code Co-authored-by: Copilot --- .gitignore | 3 +++ Ammeters/client.py | 9 ++++++--- src/testing/test_framework.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 83972fa..6d6e859 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,6 @@ __marimo__/ # Streamlit .streamlit/secrets.toml + +# Gemini AI configuration +.gemini/ \ No newline at end of file diff --git a/Ammeters/client.py b/Ammeters/client.py index e2de2ae..3c26163 100644 --- a/Ammeters/client.py +++ b/Ammeters/client.py @@ -1,13 +1,16 @@ from socket import socket, AF_INET, SOCK_STREAM +from typing import Optional - -def request_current_from_ammeter(port: int, command: bytes): +def request_current_from_ammeter(port: int, command: bytes) -> Optional[float]: with socket(AF_INET, SOCK_STREAM) as s: s.connect(('localhost', port)) s.sendall(command) data = s.recv(1024) if data: - print(f"Received current measurement from port {port}: {data.decode('utf-8')} A") + current = data.decode('utf-8') + print(f"Received current measurement from port {port}: {current} A") + return float(current) else: print("No data received.") + return None diff --git a/src/testing/test_framework.py b/src/testing/test_framework.py index 6d5b202..df478ba 100644 --- a/src/testing/test_framework.py +++ b/src/testing/test_framework.py @@ -20,7 +20,7 @@ def get_single_reading(self, ammeter_type: str) -> Optional[float]: port = ammeter_config['port'] command = ammeter_config['command'].encode('utf-8') - request_current_from_ammeter(port, command) + return request_current_from_ammeter(port, command) def run_test(self, ammeter_type: str) -> Dict: pass \ No newline at end of file From 0274e83063890ab84e07d798814c666adb1f2dc7 Mon Sep 17 00:00:00 2001 From: netanelC Date: Mon, 4 May 2026 22:06:08 +0300 Subject: [PATCH 3/6] docs: add documentation on what has been made --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9415805..ee134d3 100644 --- a/README.md +++ b/README.md @@ -50,5 +50,26 @@ This project provides emulators for different types of ammeters: Greenlee, ENTES To start the ammeter emulators and request current measurements, run the `main.py` script: ```sh -python main.py -``` \ No newline at end of file +python3 main.py +``` + +--- + +## Design Decisions & Bug Fixes + +### 1. Emulator Communication Fix (PR #10) +**The Problem:** The initial `main.py` script failed to fetch data from the ammeter emulators. +**The Fix:** +- Discovered discrepancies between the documented ports/commands, `main.py`, and the actual `Ammeters/*_Ammeter.py` implementations. +- Unified the configuration to make `config/config.yaml` and the emulator classes the source of truth. +- Set the ammeters to listen sequentially on ports `5000` (Greenlee), `5001` (ENTES), and `5002` (CIRCUTOR). +- Updated `main.py` to send the correct byte strings (e.g., `b'MEASURE_CIRCUTOR -get_measurement'`). +- Added `socket.SO_REUSEADDR` to `base_ammeter.py` to prevent "Address already in use" errors during rapid test iterations. + +### 2. Unified Testing API (Current) +**The Problem:** The exam requires a unified interface capable of communicating consistently with multiple ammeter types. +**The Design:** +- Implemented `AmmeterTestFramework` in `src/testing/test_framework.py`. +- The framework acts as an abstraction layer; users simply call `get_single_reading('greenlee')` without needing to manage raw sockets, ports, or byte commands. +- The framework dynamically reads the required connection parameters from `config/config.yaml`. +- The base `client.py` was updated to decode and return standard Python `float` types rather than printing to stdout, enabling programmatic data aggregation. \ No newline at end of file From 59cbc308e3406caea8e524480c559b600805e1f0 Mon Sep 17 00:00:00 2001 From: netanelC Date: Mon, 4 May 2026 22:42:43 +0300 Subject: [PATCH 4/6] test: add tests --- main.py | 11 +++---- tests/integration/test_api.py | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_api.py diff --git a/main.py b/main.py index 015f948..f4947d7 100644 --- a/main.py +++ b/main.py @@ -19,17 +19,18 @@ def run_circutor_emulator(): circutor = CircutorAmmeter(5002) circutor.start_server() -if __name__ == "__main__": +def start_emulators(): # Start each ammeter in a separate thread threading.Thread(target=run_greenlee_emulator, daemon=True).start() threading.Thread(target=run_entes_emulator, daemon=True).start() threading.Thread(target=run_circutor_emulator, daemon=True).start() - - # This section is commented out because it shouldn't work. - # Read the README.md file as well as the source code if you need, and fix the problem. - + # Wait for the servers to start, if you have problem restarting the servers between runs try increasing sleep time. time.sleep(5) + +if __name__ == "__main__": + start_emulators() + request_current_from_ammeter(5000, b'MEASURE_GREENLEE -get_measurement') # Request from Greenlee Ammeter request_current_from_ammeter(5001, b'MEASURE_ENTES -get_data') # Request from ENTES Ammeter request_current_from_ammeter(5002, b'MEASURE_CIRCUTOR -get_measurement') # Request from CIRCUTOR Ammeter diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py new file mode 100644 index 0000000..6a34492 --- /dev/null +++ b/tests/integration/test_api.py @@ -0,0 +1,54 @@ +import sys +import os + +# Ensure the root directory is in the python path +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')) +sys.path.insert(0, project_root) + +from main import start_emulators +from src.testing.test_framework import AmmeterTestFramework + +if __name__ == "__main__": + print("Starting ammeter emulators in the background...") + start_emulators() + print("Emulators started. Testing the new API...\n") + + # Initialize your new framework + # Note: We need to pass the absolute path to the config file so it works from any directory + config_path = os.path.join(project_root, 'config', 'config.yaml') + framework = AmmeterTestFramework(config_path=config_path) + + failures = 0 + + # Test reading from each valid type + for ammeter in ['greenlee', 'entes', 'circutor']: + print(f"--- Requesting reading from {ammeter.upper()} ---") + try: + value = framework.get_single_reading(ammeter) + if value is None: + print(f"❌ Failed: API returned None instead of a value for {ammeter}\n") + failures += 1 + else: + print(f"✅ Success! API returned type: {type(value).__name__}, value: {value}\n") + except Exception as e: + print(f"❌ Failed to fetch {ammeter}: {e}\n") + failures += 1 + + # Test reading from an invalid type (Expected to fail) + print(f"--- Requesting reading from UNKNOWN (Expected to raise ValueError) ---") + try: + framework.get_single_reading('unknown') + print(f"❌ Failed: Expected ValueError, but reading succeeded.\n") + failures += 1 + except ValueError as e: + print(f"✅ Success! API correctly raised ValueError for unknown ammeter.\n") + except Exception as e: + print(f"❌ Failed: Expected ValueError, but got {type(e).__name__}: {e}\n") + failures += 1 + + if failures > 0: + print(f"Integration tests failed with {failures} error(s).") + sys.exit(1) + else: + print("All integration tests passed successfully!") + sys.exit(0) From acb291c60b6cfcc8db16b04e1893eff35e8b40cb Mon Sep 17 00:00:00 2001 From: netanelC Date: Mon, 4 May 2026 22:43:01 +0300 Subject: [PATCH 5/6] ci: add pr check --- .github/workflows/pull-request.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/pull-request.yaml diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..548104d --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,21 @@ +name: Ammeter Test Framework CI + +on: pull_request + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Install dependencies + run: python -m pip install --upgrade pip + + - name: Run Integration Tests + run: python tests/integration/test_api.py From 1ca1df03f5a78100a67a0e5c96ece419b9a0791d Mon Sep 17 00:00:00 2001 From: netanelC Date: Mon, 4 May 2026 22:47:18 +0300 Subject: [PATCH 6/6] ci: fix installations --- .github/workflows/pull-request.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 548104d..1ccf59b 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -15,7 +15,9 @@ jobs: python-version: '3.10' - name: Install dependencies - run: python -m pip install --upgrade pip + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt - name: Run Integration Tests run: python tests/integration/test_api.py