Skip to content

test(analyses): liquid classes and 8.4.0 analyses battery testing #17884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dd1ba97
test(ab): add 96 channel happy path tests
y3rsh Mar 4, 2025
9aeef80
fix(analyses-snapshot-testing): heal lc-snapshot-protocols-96-happy-p…
github-actions[bot] Mar 4, 2025
89e3afa
adjust snapshot extension for liquid class output
y3rsh Mar 5, 2025
c52207c
fix(analyses-snapshot-testing): heal lc-snapshot-protocols-96-happy-p…
github-actions[bot] Mar 5, 2025
d55bb00
multiple locations for distribute
y3rsh Mar 7, 2025
69fdc04
format
y3rsh Mar 7, 2025
f3b7c82
fix(analyses-snapshot-testing): heal lc-snapshot-protocols-96-happy-p…
github-actions[bot] Mar 10, 2025
972cb47
fix(analyses-snapshot-testing): heal lc-snapshot-protocols-96-happy-p…
github-actions[bot] Mar 12, 2025
1b26cb8
test(ab): 8channel and single channel liquid class combinations (#17763)
y3rsh Mar 20, 2025
7ab2766
Update snapshot test files
y3rsh Mar 20, 2025
1a26f7d
backup
y3rsh Mar 21, 2025
fe15023
jira and lint
y3rsh Mar 25, 2025
c7ffbd0
Update snapshot tests
y3rsh Mar 25, 2025
7ca5028
running
y3rsh Mar 25, 2025
8624b74
added
y3rsh Mar 27, 2025
4283c05
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh Mar 27, 2025
48505dd
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh Mar 31, 2025
247bf3d
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh Apr 1, 2025
d7fc336
fixed bugs and source and dest
y3rsh Apr 1, 2025
2d9b9bf
wip
y3rsh Apr 9, 2025
10196b7
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh Apr 9, 2025
e89a28e
backup
y3rsh Apr 9, 2025
af2009f
live full
y3rsh Apr 9, 2025
2c3d530
update changes
y3rsh Apr 9, 2025
cc132e0
live full 96 ready
y3rsh Apr 9, 2025
62d2203
update changes
y3rsh Apr 9, 2025
d2806da
combos
y3rsh Apr 11, 2025
4dd7b5a
backup
y3rsh Apr 11, 2025
8dc994f
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh Apr 16, 2025
2cdd3b9
backup
y3rsh Apr 16, 2025
34d46fa
backup
y3rsh Apr 17, 2025
0ccc943
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh May 9, 2025
c547430
Merge branch 'chore_release-8.4.0' into lc-snapshot-protocols-96-happ…
y3rsh May 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions analyses-snapshot-testing/automation/analyze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# /// script
# requires-python = "==3.10.*"
# dependencies = [
# "opentrons @ /root/github/opentrons2/api",
# "opentrons-shared-data @ /root/github/opentrons2/shared-data/python",
# "rich",
# ]
# ///


import asyncio
import io
import json
import random
from contextlib import redirect_stderr, redirect_stdout
from dataclasses import dataclass
from pathlib import Path
from typing import Any, List

import anyio

# Import the internal async analysis function and the _Output class.
from opentrons.cli.analyze import _analyze, _Output # type: ignore[import-not-found]

# Imports for pretty printing with Rich.
from rich import print as rprint
from rich.panel import Panel
from rich.progress import Progress

# Constants
CUSTOM_LABWARE_DIR = Path(__file__).parent.parent / "labware"
ANALYSIS_TIMEOUT = 60 # Timeout per protocol in seconds

custom_labware_files = list(CUSTOM_LABWARE_DIR.glob("*.json"))


@dataclass
class AnalysisResult:
"""Dataclass to hold the analysis result."""

protocol_file: Path
result: dict[str, Any] | str
logs: str = ""


def run_analyze_in_thread(files: List[Path], rtp_values: str, rtp_files: str, check: bool) -> tuple[int, dict[str, Any], str]:
"""
Run _analyze in its own event loop in this thread.
This helper creates its own BytesIO stream for JSON output, traps any print output,
runs _analyze, then reads and returns the exit code, JSON result, and captured logs.
"""
# Capture prints that _analyze might do.
captured_output = io.StringIO()
json_output_stream = io.BytesIO()
outputs = [_Output(to_file=json_output_stream, kind="json")]

with redirect_stdout(captured_output), redirect_stderr(captured_output):
exit_code = asyncio.run(_analyze(files, rtp_values, rtp_files, outputs, check))

# Get the JSON output.
json_output_stream.seek(0)
json_bytes = json_output_stream.read()
try:
json_str = json_bytes.decode("utf-8")
result_json = json.loads(json_str)
except Exception:
result_json = {"error": "Failed to decode JSON output"}
# Get the captured print output.
log_output = captured_output.getvalue()
return exit_code, result_json, log_output


async def run_analysis(
file: Path,
rtp_values: str = "{}",
rtp_files: str = "{}",
check: bool = False,
) -> AnalysisResult:
"""
Run protocol analysis programmatically and return the analysis results as in-memory JSON.

This function analyzes a given protocol file in conjunction with custom labware files.
It traps printed output from _analyze and returns that as well.

Args:
file: The protocol file to analyze.
rtp_values: JSON string mapping runtime parameter variable names to values.
rtp_files: JSON string mapping runtime parameter variable names to file paths.
check: If True, returns a non-zero exit code if the analysis encountered errors.

Returns:
An AnalysisResult containing the protocol file, a dictionary with the analysis result,
and any captured log output.
"""
protocol_file = file
files = custom_labware_files + [protocol_file]

try:
# Run the analysis in a separate thread and enforce a timeout.
async with anyio.fail_after(ANALYSIS_TIMEOUT): # type: ignore
exit_code, result_json, log_output = await anyio.to_thread.run_sync(run_analyze_in_thread, files, rtp_values, rtp_files, check)
except TimeoutError:
result_json = {"error": f"Analysis timed out after {ANALYSIS_TIMEOUT} seconds"}
return AnalysisResult(protocol_file=protocol_file, result=result_json, logs="")
except Exception as e:
result_json = {"error": f"Analysis failed with error: {str(e)}"}
return AnalysisResult(protocol_file=protocol_file, result=result_json, logs="")

return AnalysisResult(protocol_file=protocol_file, result=result_json, logs=log_output)


async def main() -> None: # noqa: C901
current_dir = Path(__file__).parent
protocol_files = [
file
for file in (list(current_dir.glob("*.py")) + list(current_dir.glob("*.json")))
if "Overrides" not in file.name and "_X_" not in file.name
]
ignored_files = [
"Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3.py",
"Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3.py",
"pl_sample_dilution_with_96_channel_pipette.py",
"pl_langone_ribo_pt1_ramp.py",
]
protocol_files = [file for file in protocol_files if file.name not in ignored_files]

# Select 10 random protocol files.
protocol_files = random.sample(protocol_files, 10)
results: List[AnalysisResult] = []

# Create a Rich progress bar.
with Progress() as progress:
task_id = progress.add_task("[cyan]Processing protocols...", total=len(protocol_files))

async def run_and_collect(file: Path) -> None:
try:
result = await run_analysis(file)
except Exception as e:
result = AnalysisResult(protocol_file=file, result={"error": str(e)}, logs="")
results.append(result)
progress.advance(task_id)

async with anyio.create_task_group() as tg:
for file in protocol_files:
tg.start_soon(run_and_collect, file)

rprint(Panel("[bold cyan]All Protocol Analyses Completed[/bold cyan]"))
for result in results:
rprint(f"\n[bold blue]Protocol: {result.protocol_file.name}[/bold blue]")
if isinstance(result.result, str):
rprint(Panel(f"[bold red]Error: {result.result}[/bold red]"))
elif isinstance(result.result, dict):
status = result.result.get("result", "Unknown status")
color = "green" if status == "ok" else "red"
rprint(Panel(f"[bold {color}]Status: {status}[/bold {color}]"))

# Look for errors or warnings in the result
if "errors" in result.result and result.result["errors"]:
rprint("[bold red]Errors:[/bold red]")
for error in result.result["errors"]:
rprint(f" - {error}")
if "warnings" in result.result and result.result["warnings"]:
rprint("[bold yellow]Warnings:[/bold yellow]")
for warning in result.result["warnings"]:
rprint(f" - {warning}")

# Optionally, also print the captured log output
# if result.logs:
# rprint(Panel(f"[dim]{result.logs}[/dim]", title="Captured Logs"))


if __name__ == "__main__":
anyio.run(main)
Loading
Loading