Skip to main content
Python pytest test runner connected to a PicoScope oscilloscope on an Indian production test bench

Building an Automated Production Test Framework with pyPicoSDK and pytest: for Indian Hardware Teams

GSAS Editorial · · 8 min read

The Indian hardware engineering teams that scale successfully from prototype to volume production all have one thing in common: they stop thinking of oscilloscope measurements as “set up the bench, press the button, read the number off the screen” and start thinking of them as executable tests that run in CI alongside every firmware build. The hardware test discipline catches up with the software test discipline, and measurement becomes repeatable, version-controlled, and auditable.

pyPicoSDK is the Python wrapper around Pico Technology’s native PicoScope drivers, and it turns the PicoScope family, from the entry-level PicoScope 2000B up to the 6000E flagship, into a library a Python test suite can import. Combined with pytest, the de facto standard Python test framework, you get a production test harness that grows cleanly from 10 tests on a startup’s first prototype to 500+ tests on a mature product’s release-gate test suite.

This post is for Indian hardware engineering managers, test engineers, and firmware leads who want to know what a production-grade pytest + pyPicoSDK setup actually looks like, not a hobby-level snippet that works on one bench, but a framework that holds up when five test engineers across Bengaluru, Pune, and Chennai are all running the same suite on different hardware revisions. GSAS Micro Systems is India’s authorized Pico Technology partner, PicoScope hardware, hands-on pyPicoSDK bring-up support, and Python test integration consulting are available from our six India offices.

Why pytest is the right Python test framework for hardware test suites

Python has three major test frameworks: unittest (in the stdlib), nose (deprecated), and pytest. For hardware test suites specifically, pytest wins on three features that matter:

  1. Fixtures. pytest fixtures let you set up complex instrument state (open a PicoScope, configure channels, register buffers, establish a baseline) once at session scope, and every test that needs that state declares the fixture as a dependency. Teardown happens automatically when the session ends. This is the right abstraction for hardware resources, you do not want to re-open the USB connection 500 times across 500 tests.
  2. Parameterization. A single test function can be invoked with dozens of different input parameters (different bandwidths, different trigger levels, different signal frequencies) via @pytest.mark.parametrize. The framework treats each parameterization as a distinct test, reports them separately, and lets you focus on one variant when debugging.
  3. Plugins. pytest-xdist (parallel execution), pytest-rerunfailures (retry flaky tests), pytest-html (human-readable reports), and pytest-junit (CI integration via JUnit XML) all exist as first-party plugins. A hardware team does not have to build these from scratch.

Folder layout for a production pytest + pyPicoSDK suite

The directory structure that scales for an Indian hardware team:

test-suite/
├── conftest.py             # session-scoped fixtures (PicoScope connection,
│                           # signal generator, DUT power supply, test report)
├── lib/
│   ├── __init__.py
│   ├── picoscope_helpers.py   # pyPicoSDK abstraction layer
│   ├── measurement_helpers.py  # measurement primitives (Vpp, rise time, FFT peak)
│   ├── tolerance.py            # test-limit schema and comparison
│   └── reporting.py            # structured result logging for MES integration
├── tests/
│   ├── test_power_rails.py     # DC voltage sanity
│   ├── test_clock_integrity.py # clock signal timing
│   ├── test_can_decode.py      # CAN bus protocol decode
│   ├── test_rise_fall_times.py # digital edge timing
│   ├── test_pwm_duty.py        # motor drive PWM validation
│   └── ...
├── test_limits/
│   ├── default.yaml            # default pass/fail thresholds
│   └── variant_a.yaml          # per-variant overrides
├── requirements.txt
└── pytest.ini                  # pytest config + markers

This layout separates three concerns: the lib/ directory holds reusable measurement primitives, the tests/ directory holds executable test cases that call into lib/, and test_limits/ holds YAML files that define the pass/fail thresholds as data rather than hard-coded numbers in Python code. Variants of the same product (consumer vs industrial, 230 V vs 110 V mains, BLE vs Wi-Fi) get different YAML files with the same test functions operating against them.

The conftest.py fixtures: hardware setup once per session

# conftest.py
import pytest
from picosdk.ps5000a import ps5000a as ps
from picosdk.functions import assert_pico_ok
from lib.picoscope_helpers import PicoScope5000D

@pytest.fixture(scope="session")
def picoscope():
    """Open the PicoScope once per test session."""
    scope = PicoScope5000D()
    scope.open(resolution_bits=14)
    yield scope
    scope.close()

@pytest.fixture(scope="session")
def test_limits(pytestconfig):
    """Load the pass/fail thresholds for this test run."""
    import yaml
    variant = pytestconfig.getoption("--variant", default="default")
    with open(f"test_limits/{variant}.yaml") as f:
        return yaml.safe_load(f)

@pytest.fixture(scope="function")
def scope_ready(picoscope):
    """Reset the scope to a known configuration before each test."""
    picoscope.reset_channels()
    picoscope.clear_trigger()
    yield picoscope
    # Optional: snapshot failed-test waveforms to disk for later analysis

Session-scoped fixtures (the PicoScope connection, the test limits) run once at the start of the test run. Function-scoped fixtures (the per-test scope reset) run before every test. This gives you fast test execution, the USB handshake happens once, and clean isolation between tests.

Parameterized test cases: one function, many scenarios

# tests/test_rise_fall_times.py
import pytest
from lib.measurement_helpers import measure_rise_time

@pytest.mark.parametrize("signal_name,expected_rise_ns,tolerance_ns", [
    ("CLK_24MHZ", 5.0, 1.0),
    ("DATA_STROBE_A", 8.0, 2.0),
    ("DATA_STROBE_B", 8.0, 2.0),
    ("RESET_N", 20.0, 5.0),
    ("INT_REQ_0", 10.0, 3.0),
])
def test_digital_rise_time(scope_ready, signal_name, expected_rise_ns, tolerance_ns):
    """Verify digital signal rise time against the per-signal spec."""
    waveform = scope_ready.capture_signal(signal_name)
    rise_ns = measure_rise_time(waveform, threshold_low=0.2, threshold_high=0.8)
    assert abs(rise_ns - expected_rise_ns) <= tolerance_ns, \
        f"{signal_name} rise time {rise_ns:.1f} ns out of tolerance [{expected_rise_ns - tolerance_ns:.1f}, {expected_rise_ns + tolerance_ns:.1f}]"

pytest treats each tuple in the parametrize list as a separate test case. A run of this file reports 5 test results, one per signal, and a failure in DATA_STROBE_B does not mask the result for the other 4. For an Indian production test bench where a specific signal is problematic on one hardware revision, this isolation is exactly what the test engineer needs to triage the failure without re-running the entire suite.

Capture + measure helper pattern

The lib/picoscope_helpers.py and lib/measurement_helpers.py modules wrap the raw pyPicoSDK calls in something a test engineer can read:

# lib/picoscope_helpers.py
class PicoScope5000D:
    def __init__(self):
        self.handle = None

    def open(self, resolution_bits=14):
        self.handle = ctypes.c_int16()
        status = ps.ps5000aOpenUnit(
            ctypes.byref(self.handle),
            None,
            ps.PS5000A_DEVICE_RESOLUTION[f"PS5000A_DR_{resolution_bits}BIT"],
        )
        assert_pico_ok(status)

    def capture_signal(self, channel, range_volts, sample_count, sample_interval_ns):
        """Block-mode capture returning a NumPy array."""
        # ... pyPicoSDK setup, trigger, capture, readback ...
        return waveform_numpy_array

    def close(self):
        if self.handle:
            ps.ps5000aCloseUnit(self.handle)
# lib/measurement_helpers.py
import numpy as np

def measure_rise_time(waveform, sample_rate, threshold_low=0.2, threshold_high=0.8):
    """Measure 20-80 % rise time in nanoseconds."""
    vmin, vmax = waveform.min(), waveform.max()
    low = vmin + threshold_low * (vmax - vmin)
    high = vmin + threshold_high * (vmax - vmin)
    low_idx = np.argmax(waveform > low)
    high_idx = np.argmax(waveform > high)
    return (high_idx - low_idx) * (1e9 / sample_rate)

def measure_vpp(waveform):
    """Peak-to-peak voltage."""
    return waveform.max() - waveform.min()

def measure_fft_peak(waveform, sample_rate):
    """Dominant frequency component in hertz."""
    spectrum = np.abs(np.fft.rfft(waveform))
    peak_bin = np.argmax(spectrum[1:]) + 1
    return peak_bin * sample_rate / len(waveform)

These helpers are small, testable, and reusable across every test in the suite. When a new test engineer joins the team, the learning curve is “read measurement_helpers.py” rather than “understand the PicoSDK C API”.

CI integration: JUnit XML and pytest-html

For teams wiring the pytest + pyPicoSDK suite into a CI pipeline (Jenkins, GitHub Actions, GitLab CI, or a self-hosted runner at the production line), two output formats matter:

  • JUnit XML: pytest --junit-xml=results.xml produces a machine-readable results file that Jenkins, GitLab, and GitHub Actions all know how to parse and display as a test report. Failures show up in the build UI with stack traces and assertion messages.
  • HTML report: pytest --html=report.html --self-contained-html produces a human-readable report with every test’s status, duration, and captured stdout/stderr. This is the report format test engineers share with manufacturing engineering when a failure needs debugging.

For an Indian EMS line running the same test suite across thousands of boards per shift, pair pytest with pytest-xdist (parallel execution across multiple PicoScope benches) to scale linearly with the number of scopes in the test rack.

Integration with MES: per-unit test result upload

For Indian contract manufacturing lines that need per-unit traceability, medical under ISO 13485, automotive under IATF 16949, defence under DGQA, the pytest suite’s JUnit XML output is the raw material for MES upload. A small post-run script parses the JUnit XML, extracts per-test results keyed by the DUT serial number (captured at the start of the test session from a barcode scanner or device UID read), and POSTs the result to the line’s MES endpoint.

This closes the loop between the measurement the test engineer designed in test_rise_fall_times.py and the compliance record the auditor reviews three months later during a plant visit.

Hardware recommendations for Indian test benches

Further reading

Interested in Pico Technology tools?

Talk to our application engineers for personalized tool recommendations.

Stay in the Loop

Get monthly compliance updates, product insights, and engineering best practices delivered to your inbox.

Related Articles

Indian EV battery cell test bench with PicoScope 4444 differential probes monitoring a lithium cell
pypicosdk Pico Technology Automotive & Mobility

pyPicoSDK for EV Battery Characterization in India: Automated Cell Testing and Long-Duration Data Capture

Indian EV battery engineering teams, at Ola, Ather, Exponent, Log9, TVS, Bajaj, and the Pune-Chennai Tier-1 supplier base, use pyPicoSDK with PicoScope 4444 differential oscilloscopes and PicoLog data loggers to capture cell-level current, voltage, and temperature during drive cycles, thermal soak, and abuse tests. Here's the Python pattern that scales from a single cell to a 96-cell pack.

14 Apr 2026 · 9 min read
Indian research engineer running a pyPicoSDK streaming capture on a PicoScope 6000E oscilloscope
pypicosdk Pico Technology

pyPicoSDK Streaming Mode: Continuous Data Acquisition with PicoScope for Indian Research Labs

How Indian university research labs and industrial R&D teams use pyPicoSDK streaming mode to capture uninterrupted signal data from PicoScope 5000D, 6000E, and 4000 series oscilloscopes, with Python patterns for buffer management, disk persistence, and signal processing hand-off to NumPy and SciPy.

14 Apr 2026 · 8 min read
Benchtop and USB/PC oscilloscopes displaying multi-channel waveforms beside a probed circuit board on an Indian engineering bench, oscilloscope buying guide from GSAS Micro Systems
Test & Measurement Pico Technology GW Instek

How to Buy an Oscilloscope in India (2026): USB vs Benchtop, Bit Depth, Bandwidth: A Buying Guide

A vendor-neutral buying guide for Indian engineering teams choosing an oscilloscope in 2026. How to size bandwidth, sample rate, channels and bit depth; when a USB/PC oscilloscope beats a benchtop; and how oscilloscope price in India actually breaks down once you add GST, probes and warranty. Written for embedded, power and education teams in Bengaluru, Pune, Chennai, Hyderabad, Mumbai and Delhi NCR.

5 Jun 2026 · 11 min read