Refactor: rename SimDUT, restructure DUT simulation and test flow
This commit is contained in:
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,3 +1,39 @@
|
|||||||
.vscode/
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Pytest cache
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Test results and logs
|
||||||
|
*.log
|
||||||
|
*.csv
|
||||||
|
*.html
|
||||||
|
|
||||||
|
# VS Code settings
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Jupyter and IPython
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
.profile
|
||||||
|
|
||||||
|
# Sphinx documentation build
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
13
Makefile
13
Makefile
@@ -0,0 +1,13 @@
|
|||||||
|
# docs/Makefile
|
||||||
|
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
.PHONY: html clean
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(SOURCEDIR) $(BUILDDIR)/html
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -0,0 +1,76 @@
|
|||||||
|
# Hardware Test Framework Template
|
||||||
|
|
||||||
|
A reusable, Python-based testing framework designed for validating electronic hardware and DUTs. Built around `pytest`, this template supports serial, SCPI, and simulated devices, with structured output for automated test reporting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Pytest integration for structured and repeatable test execution
|
||||||
|
- ⚡ Interfaces with bench equipment via SCPI or serial
|
||||||
|
- 🔄 Optional simulator backend for offline development
|
||||||
|
- 📊 HTML and CSV test reports
|
||||||
|
- 🧩 Modular architecture: easy to extend for new instruments or DUTs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
src/
|
||||||
|
interfaces/ # Abstract communication backends (SCPI, Serial, Dummy)
|
||||||
|
instruments/ # Power supplies, eloads, DUT control logic
|
||||||
|
utils/ # Logging, timing, CSV writing
|
||||||
|
config/ # TOML/YAML config for sim/real hardware
|
||||||
|
|
||||||
|
tests/ # Pytest test cases
|
||||||
|
docs/ # Sphinx documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
# Set up venv (optional but recommended)
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.venv\Scripts\Activate.ps1 on Windows powershell
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
pytest --sim
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
To build HTML docs:
|
||||||
|
|
||||||
|
cd docs
|
||||||
|
make html
|
||||||
|
|
||||||
|
in windows:
|
||||||
|
|
||||||
|
cd docs
|
||||||
|
sphinx-build -b html . _build/html
|
||||||
|
|
||||||
|
Generated docs will be in docs/_build/html/index.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Nathan Corwin — nathan@corwin.life
|
||||||
|
|||||||
25
conftest.py
25
conftest.py
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
from src.instruments.real_dut import RealDUT
|
||||||
|
from src.instruments.simulated_dut import SimulatedDUT
|
||||||
|
from src.instruments.power_supply import PowerSupply
|
||||||
|
from src.interfaces.dummy import DummyBackend
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--sim", action="store_true", help="Use simulation mode")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sim_mode(request):
|
||||||
|
return request.config.getoption("--sim")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dut(sim_mode):
|
||||||
|
if sim_mode:
|
||||||
|
return SimulatedDUT()
|
||||||
|
return RealDUT()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def power_supply(sim_mode):
|
||||||
|
if sim_mode:
|
||||||
|
return PowerSupply(DummyBackend())
|
||||||
|
# TODO: Replace with real SCPI interface when ready
|
||||||
|
raise NotImplementedError("Real power supply interface not yet implemented")
|
||||||
43
docs/_static/custom.css
vendored
Normal file
43
docs/_static/custom.css
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* custom.css: Modernized Sphinx Theme Tweaks */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-color: #fefefe;
|
||||||
|
color: #333;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
color: #005f73;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0a9396;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rst-content table.docutils {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rst-content table.docutils th,
|
||||||
|
.rst-content table.docutils td {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Architecture
|
||||||
|
============
|
||||||
|
|
||||||
|
(coming soon)
|
||||||
|
|||||||
20
docs/conf.py
20
docs/conf.py
@@ -0,0 +1,20 @@
|
|||||||
|
# docs/conf.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
project = 'Hardware Test Framework'
|
||||||
|
author = 'Nathan Corwin'
|
||||||
|
release = '0.1'
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
]
|
||||||
|
|
||||||
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
html_static_path = ['_static']
|
||||||
|
html_css_files = ['custom.css']
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
This guide helps you bootstrap your first test project using the framework.
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
1. Clone the repo:
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
git clone ssh://git@git.corwin.life:23231/hw-test-template.git
|
||||||
|
|
||||||
|
2. Create a virtual environment and install dependencies:
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
3. Run a simulation-mode test:
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pytest --sim
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
|
- Edit or extend the `SimDUT` class for your DUT logic
|
||||||
|
- Add custom test cases in `tests/`
|
||||||
|
- Build docs using:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sphinx-build -b html . _build/html
|
||||||
|
|||||||
31
docs/hardware.rst
Normal file
31
docs/hardware.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Real Hardware Integration
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This page describes how to implement real instrument interfaces.
|
||||||
|
|
||||||
|
Backends
|
||||||
|
--------
|
||||||
|
|
||||||
|
Interfaces expected:
|
||||||
|
- Serial-based DUT communication
|
||||||
|
- SCPI commands over USB or LAN
|
||||||
|
- Future support for Modbus, I2C, or CAN (if needed)
|
||||||
|
|
||||||
|
Adding a Real DUT
|
||||||
|
------------------
|
||||||
|
|
||||||
|
1. Create a `RealDUT` class in `src/instruments/dut_controller.py`
|
||||||
|
2. Match the interface used in `SimDUT`
|
||||||
|
3. Uncomment the fixture in `conftest.py` to enable
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# return RealDUT(port="/dev/ttyUSB0")
|
||||||
|
|
||||||
|
Safety Tips
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- Use proper delays when enabling outputs
|
||||||
|
- Confirm settings before triggering loads
|
||||||
|
- Log all commands during development
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Welcome to the Hardware Test Framework!
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
getting_started
|
||||||
|
usage
|
||||||
|
architecture
|
||||||
|
simulation
|
||||||
|
hardware
|
||||||
|
modules
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
This project is a reusable Python test harness for validating electronic hardware designs using Pytest, SCPI/serial interfaces, and optional simulation mode.
|
||||||
|
|
||||||
|
* Easy to extend
|
||||||
|
* Supports both bench instruments and DUT interaction
|
||||||
|
* Includes automatic HTML test reporting
|
||||||
|
|||||||
22
docs/modules.rst
Normal file
22
docs/modules.rst
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Module Reference
|
||||||
|
================
|
||||||
|
|
||||||
|
This section provides an overview of Python modules in the framework.
|
||||||
|
|
||||||
|
Instrument Interfaces
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: src.instruments.dut_controller
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Utilities
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. automodule:: src.utils.logger
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Serial Interfaces
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. automodule:: src.interfaces.serial
|
||||||
|
:members:
|
||||||
23
docs/simulation.rst
Normal file
23
docs/simulation.rst
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Simulation Mode
|
||||||
|
===============
|
||||||
|
|
||||||
|
The simulation backend allows you to test framework logic without connecting real hardware.
|
||||||
|
|
||||||
|
How It Works
|
||||||
|
------------
|
||||||
|
|
||||||
|
Fixtures like `dut` use mock classes defined in `src/instruments/dut_controller.py`.
|
||||||
|
|
||||||
|
Example Methods:
|
||||||
|
- `get_version()`
|
||||||
|
- `read_temperature()`
|
||||||
|
- `set_output_enabled()`
|
||||||
|
|
||||||
|
Extending Sim Mode
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can expand simulation by:
|
||||||
|
- Creating additional dummy instruments
|
||||||
|
- Returning randomized or patterned values
|
||||||
|
- Adding logs to mimic hardware responses
|
||||||
|
|
||||||
31
docs/usage.rst
Normal file
31
docs/usage.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Usage Guide
|
||||||
|
===========
|
||||||
|
|
||||||
|
This section describes how to set up and run the test framework.
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
-----------
|
||||||
|
|
||||||
|
1. Clone the repo and create a virtual environment:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
2. Run tests in simulation mode:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pytest --sim
|
||||||
|
|
||||||
|
3. View the report:
|
||||||
|
|
||||||
|
- Open `report.html` in your browser.
|
||||||
|
|
||||||
|
Switching Between Sim and Hardware
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Use the `--sim` flag to control which backend is used.
|
||||||
|
The system defaults to skipping hardware tests
|
||||||
17
pytest.ini
17
pytest.ini
@@ -0,0 +1,17 @@
|
|||||||
|
[pytest]
|
||||||
|
# Add command-line options by default
|
||||||
|
addopts = --tb=short --strict-markers --html=report.html --self-contained-html
|
||||||
|
|
||||||
|
# Optional: set default HTML report location (if you use pytest-html)
|
||||||
|
# addopts = --tb=short --strict-markers --html=report.html
|
||||||
|
|
||||||
|
# Markers you plan to use in test code
|
||||||
|
markers =
|
||||||
|
sim: mark test to run in simulation mode only
|
||||||
|
hardware: mark test that requires real hardware
|
||||||
|
slow: mark test as slow (e.g., long soak or ramp tests)
|
||||||
|
sanity: mark test as quick smoke/sanity check
|
||||||
|
|
||||||
|
# Logging settings (optional)
|
||||||
|
log_cli = true
|
||||||
|
log_level = INFO
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
# Core testing
|
||||||
pytest
|
pytest
|
||||||
tomli
|
pytest-html
|
||||||
|
|
||||||
|
# Documentation
|
||||||
sphinx
|
sphinx
|
||||||
#pyvisa
|
|
||||||
#pyserial
|
# Instrumentation (comment in when needed)
|
||||||
|
pyvisa
|
||||||
|
pyserial
|
||||||
|
|||||||
17
src/instruments/dut_controller.py
Normal file
17
src/instruments/dut_controller.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# src/instruments/dut_controller.py
|
||||||
|
|
||||||
|
from src.utils.logger import init_logger
|
||||||
|
from src.instruments.simulated_dut import SimulatedDUT
|
||||||
|
# from src.instruments.real_dut import RealDUT # Enable when ready
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
def get_dut(sim: bool = False):
|
||||||
|
"""Returns a DUT instance based on sim mode."""
|
||||||
|
if sim:
|
||||||
|
logger.info("Using SimDUT")
|
||||||
|
return SimulatedDUT()
|
||||||
|
else:
|
||||||
|
logger.info("Using RealDUT")
|
||||||
|
raise NotImplementedError("Real DUT is not yet implemented.")
|
||||||
|
# return RealDUT() # Uncomment when ready
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Power Supply controller
|
||||||
|
|
||||||
|
This module provides a high-level interface to control a programmable power supply.
|
||||||
|
It supports setting voltage, enabling/disabling output, and measuring values from the supply.
|
||||||
|
Communication is delegated to a backend (e.g., SCPI, dummy simulator).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerSupply:
|
||||||
|
def __init__(self, comm):
|
||||||
|
"""
|
||||||
|
Initialize the power supply interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
comm: A communication backend object that implements `write()` and `query()` methods.
|
||||||
|
"""
|
||||||
|
self.comm = comm
|
||||||
|
logger.info("PowerSupply interface initialized.")
|
||||||
|
|
||||||
|
def set_voltage(self, voltage: float):
|
||||||
|
"""
|
||||||
|
Set the output voltage of the power supply.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
voltage: The desired voltage in volts.
|
||||||
|
"""
|
||||||
|
logger.debug(f"Setting voltage to {voltage:.2f}V")
|
||||||
|
self.comm.write(f"VOLT {voltage}")
|
||||||
|
|
||||||
|
def enable_output(self, enable: bool):
|
||||||
|
"""
|
||||||
|
Enable or disable the power supply output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable: True to enable output, False to disable.
|
||||||
|
"""
|
||||||
|
command = "OUTP ON" if enable else "OUTP OFF"
|
||||||
|
logger.debug(f"Setting output state: {command}")
|
||||||
|
self.comm.write(command)
|
||||||
|
|
||||||
|
def measure_voltage(self) -> float:
|
||||||
|
"""
|
||||||
|
Measure and return the current output voltage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The measured voltage in volts.
|
||||||
|
"""
|
||||||
|
response = self.comm.query("MEAS:VOLT?")
|
||||||
|
logger.debug(f"Measured voltage response: {response}")
|
||||||
|
try:
|
||||||
|
return float(response.strip())
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"Invalid voltage response: {response}")
|
||||||
|
return float('nan')
|
||||||
|
|||||||
22
src/instruments/real_dut.py
Normal file
22
src/instruments/real_dut.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# src/instruments/real_dut.py
|
||||||
|
|
||||||
|
from src.utils.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
class RealDUT:
|
||||||
|
"""Real Device Under Test (stub)."""
|
||||||
|
|
||||||
|
def __init__(self, port="/dev/ttyUSB0"):
|
||||||
|
self.port = port
|
||||||
|
logger.info(f"Connecting to real DUT on port {port}")
|
||||||
|
# Set up serial or network connection here
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
raise NotImplementedError("Real DUT not implemented yet.")
|
||||||
|
|
||||||
|
def read_temperature(self):
|
||||||
|
raise NotImplementedError("Real DUT not implemented yet.")
|
||||||
|
|
||||||
|
def set_output_enabled(self, enabled: bool):
|
||||||
|
raise NotImplementedError("Real DUT not implemented yet.")
|
||||||
27
src/instruments/simulated_dut.py
Normal file
27
src/instruments/simulated_dut.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# src/instruments/simulated_dut.py
|
||||||
|
|
||||||
|
from src.utils.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
class SimulatedDUT:
|
||||||
|
"""Simulated Device Under Test."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logger.info("SimDUT initialized")
|
||||||
|
self.version = "sim-1.0.0"
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
"""Return simulated firmware version string."""
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
def read_temperature(self):
|
||||||
|
"""Simulated temperature sensor readout."""
|
||||||
|
logger.debug("SimDUT returning temperature")
|
||||||
|
return 42.5
|
||||||
|
|
||||||
|
def set_output_enabled(self, enabled: bool):
|
||||||
|
"""Simulated control for DUT output."""
|
||||||
|
state = "enabled" if enabled else "disabled"
|
||||||
|
logger.debug(f"SimDUT setting output: {state}")
|
||||||
|
return f"Output {state}"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Dummy backend for instrument simulation.
|
||||||
|
|
||||||
|
This backend emulates SCPI-compatible instruments by printing or returning canned responses.
|
||||||
|
Used for simulation/testing when hardware is not connected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyBackend:
|
||||||
|
def __init__(self):
|
||||||
|
self.state = {
|
||||||
|
"VOLT": 12.0,
|
||||||
|
"OUTP": False,
|
||||||
|
}
|
||||||
|
logger.info("Initialized DummyBackend")
|
||||||
|
|
||||||
|
def write(self, command: str):
|
||||||
|
logger.debug(f"[SIM WRITE] {command}")
|
||||||
|
parts = command.strip().split()
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = parts[0].upper()
|
||||||
|
|
||||||
|
if cmd == "VOLT" and len(parts) == 2:
|
||||||
|
try:
|
||||||
|
self.state["VOLT"] = float(parts[1])
|
||||||
|
logger.info(f"[SIM] Voltage set to {self.state['VOLT']} V")
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"[SIM] Invalid voltage value: {parts[1]}")
|
||||||
|
|
||||||
|
elif cmd == "OUTP" and len(parts) == 2:
|
||||||
|
state = parts[1].upper()
|
||||||
|
self.state["OUTP"] = state == "ON"
|
||||||
|
logger.info(f"[SIM] Output {'enabled' if self.state['OUTP'] else 'disabled'}")
|
||||||
|
|
||||||
|
def query(self, command: str) -> str:
|
||||||
|
logger.debug(f"[SIM QUERY] {command}")
|
||||||
|
cmd = command.strip().upper()
|
||||||
|
|
||||||
|
if cmd == "MEAS:VOLT?":
|
||||||
|
# Simulated slight fluctuation
|
||||||
|
voltage = self.state["VOLT"] + 0.01
|
||||||
|
return f"{voltage:.2f}"
|
||||||
|
|
||||||
|
return "0"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
SCPI communication backend for test instruments using VISA.
|
||||||
|
|
||||||
|
This module provides a communication layer to talk to SCPI-compatible devices via USB, GPIB, or TCP/IP.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyvisa
|
||||||
|
except ImportError:
|
||||||
|
pyvisa = None
|
||||||
|
logger.warning("pyvisa not available. SCPI interface will not work until it's installed.")
|
||||||
|
|
||||||
|
|
||||||
|
class SCPIDevice:
|
||||||
|
def __init__(self, resource_name: str, timeout: int = 2000):
|
||||||
|
"""
|
||||||
|
Initialize SCPI communication with the specified VISA resource.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_name: VISA resource string (e.g., "USB0::0x1234::0x5678::INSTR").
|
||||||
|
timeout: Communication timeout in milliseconds.
|
||||||
|
"""
|
||||||
|
if not pyvisa:
|
||||||
|
raise ImportError("pyvisa is required for SCPI backend.")
|
||||||
|
|
||||||
|
self.rm = pyvisa.ResourceManager()
|
||||||
|
self.instrument = self.rm.open_resource(resource_name)
|
||||||
|
self.instrument.timeout = timeout
|
||||||
|
logger.info(f"SCPI instrument initialized: {resource_name}")
|
||||||
|
|
||||||
|
def write(self, command: str):
|
||||||
|
logger.debug(f"[SCPI WRITE] {command}")
|
||||||
|
self.instrument.write(command)
|
||||||
|
|
||||||
|
def query(self, command: str) -> str:
|
||||||
|
logger.debug(f"[SCPI QUERY] {command}")
|
||||||
|
return self.instrument.query(command)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Serial communication backend for DUTs and microcontrollers (e.g. Arduino).
|
||||||
|
|
||||||
|
Provides a simple wrapper around pyserial for consistent I/O behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError:
|
||||||
|
serial = None
|
||||||
|
logger.warning("pyserial not available. Serial interface will not work until it's installed.")
|
||||||
|
|
||||||
|
|
||||||
|
class SerialDevice:
|
||||||
|
def __init__(self, port: str, baudrate: int = 115200, timeout: float = 1.0):
|
||||||
|
"""
|
||||||
|
Initialize serial communication with the device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Serial port (e.g., COM3, /dev/ttyUSB0)
|
||||||
|
baudrate: Baud rate (default: 115200)
|
||||||
|
timeout: Read timeout in seconds (default: 1.0)
|
||||||
|
"""
|
||||||
|
if not serial:
|
||||||
|
raise ImportError("pyserial is required for SerialDevice backend.")
|
||||||
|
|
||||||
|
self.ser = serial.Serial(port=port, baudrate=baudrate, timeout=timeout)
|
||||||
|
logger.info(f"Serial port {port} opened at {baudrate} baud")
|
||||||
|
|
||||||
|
def write(self, data: str):
|
||||||
|
"""
|
||||||
|
Send a string over the serial connection.
|
||||||
|
"""
|
||||||
|
logger.debug(f"[SERIAL WRITE] {data.strip()}")
|
||||||
|
self.ser.write(data.encode('utf-8'))
|
||||||
|
|
||||||
|
def read_line(self) -> str:
|
||||||
|
"""
|
||||||
|
Read a single line from the serial connection.
|
||||||
|
"""
|
||||||
|
line = self.ser.readline().decode('utf-8').strip()
|
||||||
|
logger.debug(f"[SERIAL READ] {line}")
|
||||||
|
return line
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close the serial port connection.
|
||||||
|
"""
|
||||||
|
self.ser.close()
|
||||||
|
logger.info("Serial port closed")
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# src/utils/logger.py
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def init_logger(name: str = "test_framework", level: int = logging.INFO) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Initializes and returns a logger instance with a standard format.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
Name of the logger.
|
||||||
|
level : int
|
||||||
|
Logging level (e.g., logging.INFO, logging.DEBUG).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
logging.Logger
|
||||||
|
Configured logger instance.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
if not logger.handlers:
|
||||||
|
# Console handler
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(level)
|
||||||
|
|
||||||
|
# Formatter
|
||||||
|
formatter = logging.Formatter('[%(levelname)s] %(name)s: %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|||||||
14
tests/test_dut.py
Normal file
14
tests/test_dut.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# tests/test_dut.py
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_dut_version(dut):
|
||||||
|
"""Ensure the DUT returns a version string in sim mode."""
|
||||||
|
version = dut.get_version()
|
||||||
|
assert version.startswith("sim-")
|
||||||
|
|
||||||
|
def test_dut_temperature(dut):
|
||||||
|
"""Test readback of simulated temperature."""
|
||||||
|
temp = dut.read_temperature()
|
||||||
|
assert 20 <= temp <= 100 # Acceptable range
|
||||||
|
|
||||||
20
tests/test_power_supply.py
Normal file
20
tests/test_power_supply.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# tests/test_power_supply.py
|
||||||
|
"""
|
||||||
|
Basic functional tests for the PowerSupply
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_set_and_measure_voltage(power_supply):
|
||||||
|
test_voltage = 5.0
|
||||||
|
power_supply.set_voltage(test_voltage)
|
||||||
|
measured = power_supply.measure_voltage()
|
||||||
|
assert abs(measured - test_voltage) < 0.05
|
||||||
|
|
||||||
|
def test_output_toggle(power_supply):
|
||||||
|
power_supply.enable_output(True)
|
||||||
|
# Only works for DummyBackend which keeps state
|
||||||
|
if hasattr(power_supply.comm, "state"):
|
||||||
|
assert power_supply.comm.state["OUTP"] is True
|
||||||
|
|
||||||
|
power_supply.enable_output(False)
|
||||||
|
if hasattr(power_supply.comm, "state"):
|
||||||
|
assert power_supply.comm.state["OUTP"] is False
|
||||||
Reference in New Issue
Block a user