diff --git a/src/pleiades/core/data_manager.py b/src/pleiades/core/data_manager.py index 0ae644d..19926b0 100644 --- a/src/pleiades/core/data_manager.py +++ b/src/pleiades/core/data_manager.py @@ -73,9 +73,7 @@ def get_file_path(self, category: DataCategory, filename: str) -> Path: file_path = Path(filename) if file_path.suffix not in self._VALID_EXTENSIONS[category]: - raise ValueError( - f"Invalid file extension for {category}. " f"Allowed extensions: {self._VALID_EXTENSIONS[category]}" - ) + raise ValueError(f"Invalid file extension for {category}. " f"Allowed extensions: {self._VALID_EXTENSIONS[category]}") try: with resources.path(f"pleiades.data.{self._get_category_path(category)}", filename) as path: diff --git a/src/pleiades/core/models.py b/src/pleiades/core/models.py index 50f1c37..1591ed2 100644 --- a/src/pleiades/core/models.py +++ b/src/pleiades/core/models.py @@ -231,9 +231,7 @@ def areal_density(self) -> float: if self.thickness_unit == "mm": thickness_cm /= 10.0 - return ( - thickness_cm * self.density * CONSTANTS.avogadro_number / self.atomic_mass.value / 1e24 - ) # Convert to atoms/barn + return thickness_cm * self.density * CONSTANTS.avogadro_number / self.atomic_mass.value / 1e24 # Convert to atoms/barn # Unit conversion functions diff --git a/src/pleiades/sammy/backends/docker.py b/src/pleiades/sammy/backends/docker.py index 5f7609b..95f6050 100644 --- a/src/pleiades/sammy/backends/docker.py +++ b/src/pleiades/sammy/backends/docker.py @@ -48,9 +48,7 @@ def prepare_environment(self, files: SammyFiles) -> None: raise EnvironmentPreparationError("Docker not found in PATH") # Verify docker image exists - result = subprocess.run( - ["docker", "image", "inspect", self.config.image_name], capture_output=True, text=True - ) + result = subprocess.run(["docker", "image", "inspect", self.config.image_name], capture_output=True, text=True) if result.returncode != 0: raise EnvironmentPreparationError(f"Docker image not found: {self.config.image_name}") diff --git a/src/pleiades/sammy/backends/local.py b/src/pleiades/sammy/backends/local.py index 0a3ca97..edc9cfc 100644 --- a/src/pleiades/sammy/backends/local.py +++ b/src/pleiades/sammy/backends/local.py @@ -81,10 +81,7 @@ def execute_sammy(self, files: SammyFiles) -> SammyExecutionResult: if not success: logger.error(f"SAMMY execution failed for {execution_id}") - error_message = ( - f"SAMMY execution failed with return code {process.returncode}. " - "Check console output for details." - ) + error_message = f"SAMMY execution failed with return code {process.returncode}. " "Check console output for details." else: logger.info(f"SAMMY execution completed successfully for {execution_id}") error_message = None diff --git a/src/pleiades/sammy/factory.py b/src/pleiades/sammy/factory.py index f4ba5df..23134a8 100644 --- a/src/pleiades/sammy/factory.py +++ b/src/pleiades/sammy/factory.py @@ -105,9 +105,7 @@ def list_available_backends() -> Dict[BackendType, bool]: return available @classmethod - def create_runner( - cls, backend_type: str, working_dir: Path, output_dir: Optional[Path] = None, **kwargs - ) -> SammyRunner: + def create_runner(cls, backend_type: str, working_dir: Path, output_dir: Optional[Path] = None, **kwargs) -> SammyRunner: """ Create a SAMMY runner with the specified backend and configuration. @@ -295,9 +293,7 @@ def process_config(cfg): backend_config = config.get(backend_type, {}) # Create runner using create_runner - return cls.create_runner( - backend_type=backend_type, working_dir=working_dir, output_dir=output_dir, **backend_config - ) + return cls.create_runner(backend_type=backend_type, working_dir=working_dir, output_dir=output_dir, **backend_config) except Exception as e: if not isinstance(e, (ConfigurationError, BackendNotAvailableError)): @@ -347,9 +343,7 @@ def auto_select( preferred = BackendType(preferred_backend.lower()) if available[preferred]: logger.info(f"Using preferred backend: {preferred.value}") - return cls.create_runner( - backend_type=preferred.value, working_dir=working_dir, output_dir=output_dir, **kwargs - ) + return cls.create_runner(backend_type=preferred.value, working_dir=working_dir, output_dir=output_dir, **kwargs) else: logger.warning(f"Preferred backend {preferred.value} not available, " "trying alternatives") except ValueError: @@ -367,9 +361,7 @@ def auto_select( if available[backend]: try: logger.info(f"Attempting to use {backend.value} backend") - return cls.create_runner( - backend_type=backend.value, working_dir=working_dir, output_dir=output_dir, **kwargs - ) + return cls.create_runner(backend_type=backend.value, working_dir=working_dir, output_dir=output_dir, **kwargs) except Exception as e: logger.warning(f"Failed to configure {backend.value} backend: {str(e)}") errors.append(f"{backend.value}: {str(e)}") diff --git a/src/pleiades/sammy/parameters/__init__.py b/src/pleiades/sammy/parameters/__init__.py index 16abca8..e1caaba 100644 --- a/src/pleiades/sammy/parameters/__init__.py +++ b/src/pleiades/sammy/parameters/__init__.py @@ -1,8 +1,11 @@ from pleiades.sammy.parameters.broadening import BroadeningParameterCard # noqa: F401 from pleiades.sammy.parameters.data_reduction import DataReductionCard # noqa: F401 from pleiades.sammy.parameters.external_r import ExternalREntry # noqa: F401 +from pleiades.sammy.parameters.isotope import IsotopeCard # noqa: F401 from pleiades.sammy.parameters.normalization import NormalizationBackgroundCard # noqa: F401 from pleiades.sammy.parameters.orres import ORRESCard # noqa: F401 +from pleiades.sammy.parameters.paramagnetic import ParamagneticParameters # noqa: F401 from pleiades.sammy.parameters.radius import RadiusCard # noqa: F401 from pleiades.sammy.parameters.resonance import ResonanceEntry # noqa: F401 from pleiades.sammy.parameters.unused_var import UnusedCorrelatedCard # noqa: F401 +from pleiades.sammy.parameters.user_resolution import UserResolutionParameters # noqa: F401 diff --git a/src/pleiades/sammy/parameters/background.py b/src/pleiades/sammy/parameters/background.py index 3603cfa..954f36a 100644 --- a/src/pleiades/sammy/parameters/background.py +++ b/src/pleiades/sammy/parameters/background.py @@ -1,5 +1,74 @@ #!/usr/bin/env python -"""Background function card for SAMMY. +"""Parsers and containers for SAMMY's Card Set 13 background function parameters. -TODO: This card will be implemented in the future. +This module implements parsers and containers for Card Set 13 background parameters +which can appear in either the PARameter or INPut file. + +Format specification from Table VI B.2: +Card Set 13 contains background function parameters with distinct formats: +1. CONST - Constant background +2. EXPON - Exponential background +3. POWER - Power law background +4. EXPLN - Exponential with logarithmic terms +5. T-PNT - Point-wise linear function of time +6. E-PNT - Point-wise linear function of energy +7. TFILE - File-based time function +8. EFILE - File-based energy function +9. AETOB - Power of energy + +Currently unimplemented - placeholder for future development. """ + +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + + +class BackgroundType(str, Enum): + """Types of background functions available.""" + + CONST = "CONST" # Constant background + EXPON = "EXPON" # Exponential background + POWER = "POWER" # Power law background + EXPLN = "EXPLN" # Exponential with logarithmic terms + T_PNT = "T-PNT" # Point-wise linear in time + E_PNT = "E-PNT" # Point-wise linear in energy + TFILE = "TFILE" # Time function from file + EFILE = "EFILE" # Energy function from file + AETOB = "AETOB" # Power of energy + + +class BackgroundParameters(BaseModel): + """Container for Card Set 13 background function parameters. + + Currently unimplemented - placeholder for future development. + + Format specification from Table VI B.2: + Cols Format Variable Description + 1-80 A WHAT "BACKGround functions" + + Followed by one or more background function definitions. + """ + + type: BackgroundType = Field(..., description="Type of background function") + + @classmethod + def from_lines(cls, lines: List[str]) -> "BackgroundParameters": + """Parse background parameters from fixed-width format lines. + + Args: + lines: List of input lines for background parameters + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 13 background parameter parsing is not yet implemented") + + def to_lines(self) -> List[str]: + """Convert parameters to fixed-width format lines. + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 13 background parameter formatting is not yet implemented") diff --git a/src/pleiades/sammy/parameters/det_efficiency.py b/src/pleiades/sammy/parameters/det_efficiency.py index 340a22f..518c126 100644 --- a/src/pleiades/sammy/parameters/det_efficiency.py +++ b/src/pleiades/sammy/parameters/det_efficiency.py @@ -1,5 +1,68 @@ #!/usr/bin/env python -"""Detector efficiency function card for SAMMY. +"""Parsers and containers for SAMMY's Card Set 15 detector efficiency parameters. -TODO: This card will be implemented in the future. +This module implements parsers and containers for Card Set 15 detector efficiency parameters +which can appear in either the PARameter or INPut file. + +Format specification from Table VI B.2: +Card Set 15 contains detector efficiency parameters for specific spin groups. +Each efficiency value can be assigned to multiple spin groups, with any unassigned +groups taking the final efficiency in the list. + +Currently unimplemented - placeholder for future development. """ + +from typing import List + +from pydantic import BaseModel + + +class DetectorEfficiencyParameters(BaseModel): + """Container for Card Set 15 detector efficiency parameters. + + Currently unimplemented - placeholder for future development. + + Format specification from Table VI B.2: + Cols Format Variable Description + 1-80 A WHAT "DETECtor efficiencies" + + Followed by one or more efficiency definitions: + 1-10 F PARDET Detector efficiency + 11-20 F DELDET Uncertainty on efficiency + 21-22 I IFLDET Flag to vary efficiency + 23-24 I IGRDET First spin group number + 25-26 I IGRDET Second spin group number + ...etc. + + Notes: + - If more than 29 spin groups needed, insert "-1" in cols 79-80 + and continue on next line + - When more than 99 spin groups total, use 5 columns per group number + - Any spin groups not included use the final efficiency in the list + + Attributes: + efficiencies: List of efficiency values + uncertainties: List of uncertainty values + flags: List of vary flags + group_assignments: List of lists containing group numbers for each efficiency + """ + + @classmethod + def from_lines(cls, lines: List[str]) -> "DetectorEfficiencyParameters": + """Parse detector efficiency parameters from fixed-width format lines. + + Args: + lines: List of input lines for efficiency parameters + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 15 detector efficiency parameter parsing is not yet implemented") + + def to_lines(self) -> List[str]: + """Convert parameters to fixed-width format lines. + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 15 detector efficiency parameter formatting is not yet implemented") diff --git a/src/pleiades/sammy/parameters/resolution.py b/src/pleiades/sammy/parameters/resolution.py index f5ae240..f63d541 100644 --- a/src/pleiades/sammy/parameters/resolution.py +++ b/src/pleiades/sammy/parameters/resolution.py @@ -1,5 +1,134 @@ #!/usr/bin/env python -"""Resolution function card for SAMMY. +"""Parsers and containers for SAMMY's Card Set 14 resolution function parameters. -TODO: This module will be implemented in the future. +This module implements parsers and containers for Card Set 14 resolution parameters +which can appear in either the PARameter or INPut file. + +Format specification from Table VI B.2: +Card Set 14 contains resolution function parameters with distinct formats: +1. RPI Resolution function +2. GEEL resolution function +3. GELINA resolution function +4. NTOF resolution function +5. User-defined resolution function + +Each type has its own multi-line parameter structure. +Currently unimplemented - placeholder for future development. """ + +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + + +class ResolutionType(str, Enum): + """Types of resolution functions available.""" + + RPI = "RPI Resolution" + GEEL = "GEEL resolution" + GELINA = "GELINa resolution" + NTOF = "NTOF resolution" + USER = "USER-Defined resolution function" + + +class ResolutionParameters(BaseModel): + """Container for Card Set 14 resolution function parameters. + + Currently unimplemented - placeholder for future development. + + Format specification from Table VI B.2: + Different resolution types have different parameter structures: + + RPI Resolution: + - Optional burst width + - Optional tau parameters + - Optional lambda parameters + - Optional A1 parameters + - Optional exponential parameters + - Optional channel parameters + + GEEL Resolution: + - Similar structure with different defaults + + GELINA Resolution: + - Similar structure with different defaults + + NTOF Resolution: + - Similar structure with different defaults + + User-Defined Resolution: + - Custom file-based definition + """ + + type: ResolutionType = Field(..., description="Type of resolution function") + + @classmethod + def from_lines(cls, lines: List[str]) -> "ResolutionParameters": + """Parse resolution parameters from fixed-width format lines. + + Args: + lines: List of input lines for resolution parameters + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 14 resolution parameter parsing is not yet implemented") + + def to_lines(self) -> List[str]: + """Convert parameters to fixed-width format lines. + + Raises: + NotImplementedError: This class is not yet implemented + """ + raise NotImplementedError("Card Set 14 resolution parameter formatting is not yet implemented") + + +class RPIResolutionParameters(ResolutionParameters): + """Container for RPI resolution function parameters. + + Currently unimplemented - placeholder for future development. + Format includes burst width, tau, lambda, A1, exponential and channel parameters. + """ + + type: ResolutionType = ResolutionType.RPI + + +class GEELResolutionParameters(ResolutionParameters): + """Container for GEEL resolution function parameters. + + Currently unimplemented - placeholder for future development. + Similar format to RPI with different defaults. + """ + + type: ResolutionType = ResolutionType.GEEL + + +class GELINAResolutionParameters(ResolutionParameters): + """Container for GELINA resolution function parameters. + + Currently unimplemented - placeholder for future development. + Similar format to RPI with different defaults. + """ + + type: ResolutionType = ResolutionType.GELINA + + +class NTOFResolutionParameters(ResolutionParameters): + """Container for NTOF resolution function parameters. + + Currently unimplemented - placeholder for future development. + Similar format to RPI with different defaults. + """ + + type: ResolutionType = ResolutionType.NTOF + + +class UserResolutionParameters(ResolutionParameters): + """Container for user-defined resolution function parameters. + + Currently unimplemented - placeholder for future development. + Includes file-based definition capability. + """ + + type: ResolutionType = ResolutionType.USER diff --git a/src/pleiades/sammy/parameters/user_resolution.py b/src/pleiades/sammy/parameters/user_resolution.py new file mode 100644 index 0000000..32a2311 --- /dev/null +++ b/src/pleiades/sammy/parameters/user_resolution.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +"""Parser for SAMMY's Card Set 16 (User-Defined Resolution Function) parameters. + +Format specification from Table VI B.2: +Card Set 16 defines user-defined resolution functions with the following components: + +1. Header line ("USER-Defined resolution function") +2. Optional BURST line for burst width parameters +3. Optional CHANN lines for channel-dependent parameters +4. Required FILE= lines specifying data files + +Each section has specific fixed-width format requirements. +""" + +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field + +from pleiades.sammy.parameters.helper import VaryFlag, format_float, format_vary, safe_parse + +# Format definitions - column positions for each parameter type +FORMAT_SPECS = { + "BURST": { + "identifier": slice(0, 5), # "BURST" + "flag": slice(6, 7), # IFBRST + "width": slice(10, 20), # BURST value + "uncertainty": slice(20, 30), # dBURST + }, + "CHANN": { + "identifier": slice(0, 5), # "CHANN" + "flag": slice(6, 7), # ICH flag + "energy": slice(10, 20), # ECRNCH + "width": slice(20, 30), # CH value + "uncertainty": slice(30, 40), # dCH + }, + "FILE": { + "identifier": slice(0, 5), # "FILE=" + "name": slice(5, 75), # Filename + }, +} + + +class Card16ParameterType(str, Enum): + """Enumeration of Card 16 parameter types.""" + + USER = "USER" # Header identifier + BURST = "BURST" + CHANN = "CHANN" + FILE = "FILE=" + + +class UserResolutionParameters(BaseModel): + """Container for User-Defined Resolution Function parameters. + + Attributes: + type: Parameter type identifier (always "USER") + burst_width: Square burst width value (ns), optional + burst_uncertainty: Uncertainty on burst width, optional + burst_flag: Flag for varying burst width + channel_energies: List of energies for channel widths + channel_widths: List of channel width values + channel_uncertainties: List of uncertainties on channel widths + channel_flags: List of flags for varying channel widths + filenames: List of data file names + """ + + type: Card16ParameterType = Card16ParameterType.USER + burst_width: Optional[float] = Field(None, description="Square burst width (ns)") + burst_uncertainty: Optional[float] = Field(None, description="Uncertainty on burst width") + burst_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for burst width") + channel_energies: List[float] = Field(default_factory=list, description="Energies for channels") + channel_widths: List[float] = Field(default_factory=list, description="Channel width values") + channel_uncertainties: List[Optional[float]] = Field(default_factory=list, description="Channel uncertainties") + channel_flags: List[VaryFlag] = Field(default_factory=list, description="Channel flags") + filenames: List[str] = Field(default_factory=list, description="Resolution function filenames") + + @classmethod + def from_lines(cls, lines: List[str]) -> "UserResolutionParameters": + """Parse user resolution parameters from fixed-width format lines.""" + if not lines: + raise ValueError("No lines provided") + + # Verify header line + header = lines[0].strip() + if not header.startswith("USER-Defined"): + raise ValueError(f"Invalid header: {header}") + + # Initialize parameters + params = cls() + + # Process remaining lines + current_line = 1 + while current_line < len(lines): + line = f"{lines[current_line]:<80}" # Pad to full width + + # Skip blank lines + if not line.strip(): + current_line += 1 + continue + + # Parse BURST line + if line.startswith("BURST"): + # Verify identifier + identifier = line[FORMAT_SPECS["BURST"]["identifier"]].strip() + if identifier != "BURST": + raise ValueError(f"Invalid BURST identifier: {identifier}") + + # Parse flag + try: + burst_flag = VaryFlag(int(line[FORMAT_SPECS["BURST"]["flag"]].strip() or "0")) + except ValueError as e: + raise ValueError(f"Invalid BURST flag value: {e}") + + # Parse width and uncertainty + burst_width = safe_parse(line[FORMAT_SPECS["BURST"]["width"]]) + if burst_width is None: + raise ValueError("Missing required burst width value") + + burst_uncertainty = safe_parse(line[FORMAT_SPECS["BURST"]["uncertainty"]]) + + params.burst_width = burst_width + params.burst_uncertainty = burst_uncertainty + params.burst_flag = burst_flag + + elif line.startswith("CHANN"): + # Verify identifier + identifier = line[FORMAT_SPECS["CHANN"]["identifier"]].strip() + if identifier != "CHANN": + raise ValueError(f"Invalid CHANN identifier: {identifier}") + + # Parse flag + try: + flag = VaryFlag(int(line[FORMAT_SPECS["CHANN"]["flag"]].strip() or "0")) + except ValueError as e: + raise ValueError(f"Invalid CHANN flag value: {e}") + + # Parse required values + energy = safe_parse(line[FORMAT_SPECS["CHANN"]["energy"]]) + width = safe_parse(line[FORMAT_SPECS["CHANN"]["width"]]) + if energy is None or width is None: + raise ValueError("Missing required energy or width value") + + # Parse optional uncertainty + uncertainty = safe_parse(line[FORMAT_SPECS["CHANN"]["uncertainty"]]) + + # Append values to lists + params.channel_energies.append(energy) + params.channel_widths.append(width) + params.channel_uncertainties.append(uncertainty) + params.channel_flags.append(flag) + + elif line.startswith("FILE="): + # Verify identifier and format + identifier = line[FORMAT_SPECS["FILE"]["identifier"]].strip() + if identifier != "FILE=": + raise ValueError(f"Invalid FILE identifier: {identifier}") + + # Get raw filename by removing "FILE=" prefix + raw_filename = line[5:].strip() # Everything after FILE= + if not raw_filename: + raise ValueError("Missing filename") + + # Check raw filename length before column slicing + if len(raw_filename) > 70: + raise ValueError(f"Filename length ({len(raw_filename)}) exceeds maximum length (70 characters)") + + # Now we can safely use the column slice knowing it won't truncate valid data + filename = line[FORMAT_SPECS["FILE"]["name"]].strip() + params.filenames.append(filename) + + else: + raise ValueError(f"Invalid line type: {line.strip()}") + + current_line += 1 + + return params + + def to_lines(self) -> List[str]: + """Convert parameters to fixed-width format lines.""" + lines = ["USER-Defined resolution function"] + + # Add BURST line if parameters present + if self.burst_width is not None: + burst_parts = [ + "BURST", # Identifier + " ", # Column 6 spacing + format_vary(self.burst_flag), # Col 7 + " ", # Columns 8-10 spacing + format_float(self.burst_width, width=10), + format_float(self.burst_uncertainty, width=10), + ] + lines.append("".join(burst_parts)) + + # Add CHANN lines if parameters present + for i in range(len(self.channel_energies)): + channel_parts = [ + "CHANN", # Identifier + " ", # Column 6 spacing + format_vary(self.channel_flags[i]), # Col 7 + " ", # Columns 8-10 spacing + format_float(self.channel_energies[i], width=10), + format_float(self.channel_widths[i], width=10), + format_float(self.channel_uncertainties[i], width=10), + ] + lines.append("".join(channel_parts)) + + # Add FILE lines if present + for filename in self.filenames: + lines.append(f"FILE={filename}") + + # Add required blank line at end per spec + lines.append("") + + return lines + + +if __name__ == "__main__": + print("Refer to unit tests for usage examples.") diff --git a/src/pleiades/sammy/parfile.py b/src/pleiades/sammy/parfile.py index 116b79b..c05c7ed 100644 --- a/src/pleiades/sammy/parfile.py +++ b/src/pleiades/sammy/parfile.py @@ -9,11 +9,14 @@ BroadeningParameterCard, DataReductionCard, ExternalREntry, + IsotopeCard, NormalizationBackgroundCard, ORRESCard, + ParamagneticParameters, RadiusCard, ResonanceEntry, UnusedCorrelatedCard, + UserResolutionParameters, ) @@ -30,6 +33,10 @@ class SammyParameterFile(BaseModel): radius: Optional[RadiusCard] = Field(None, description="Radius parameters") data_reduction: Optional[DataReductionCard] = Field(None, description="Data reduction parameters") orres: Optional[ORRESCard] = Field(None, description="ORRES card parameters") + isotope: Optional[IsotopeCard] = Field(None, description="Isotope parameters") + paramagnetic: Optional[ParamagneticParameters] = Field(None, description="Paramagnetic parameters") + reonance: Optional[ResonanceEntry] = Field(None, description="Resonance parameters") + user_resolution: Optional[UserResolutionParameters] = Field(None, description="User-defined resolution function parameters") @classmethod def from_file(cls, file_path): diff --git a/tests/unit/pleiades/sammy/backends/test_docker.py b/tests/unit/pleiades/sammy/backends/test_docker.py index 76f0591..549f8bf 100644 --- a/tests/unit/pleiades/sammy/backends/test_docker.py +++ b/tests/unit/pleiades/sammy/backends/test_docker.py @@ -124,9 +124,7 @@ def test_execute_sammy_success(self, docker_config, mock_sammy_files, mock_subpr assert "Normal finish to SAMMY" in result.console_output assert result.error_message is None - def test_execute_sammy_failure( - self, docker_config, mock_sammy_files, mock_subprocess_docker_fail, mock_docker_command - ): + def test_execute_sammy_failure(self, docker_config, mock_sammy_files, mock_subprocess_docker_fail, mock_docker_command): """Should handle SAMMY execution failure in container.""" _ = mock_docker_command # implicitly used via fixture _ = mock_subprocess_docker_fail # implicitly used via fixture @@ -139,9 +137,7 @@ def test_execute_sammy_failure( assert not result.success assert "Docker execution failed" in result.error_message - def test_collect_outputs( - self, docker_config, mock_sammy_files, mock_subprocess_docker, mock_docker_command, mock_sammy_results - ): + def test_collect_outputs(self, docker_config, mock_sammy_files, mock_subprocess_docker, mock_docker_command, mock_sammy_results): """Should collect output files from container.""" _ = mock_docker_command # implicitly used via fixture _ = mock_subprocess_docker # implicitly used via fixture diff --git a/tests/unit/pleiades/sammy/parameters/test_background.py b/tests/unit/pleiades/sammy/parameters/test_background.py index 87ce152..7708397 100644 --- a/tests/unit/pleiades/sammy/parameters/test_background.py +++ b/tests/unit/pleiades/sammy/parameters/test_background.py @@ -1,10 +1,57 @@ #!/usr/bin/env python -"""Tests for the background function card. +"""Unit tests for Card Set 13 (Background Function Parameters) parsing. -TODO: This test will be implemented in the future when the card is implemented. +Currently tests for NotImplementedError as functionality is not yet implemented. """ import pytest +from pleiades.sammy.parameters.background import BackgroundParameters, BackgroundType + + +class TestBackgroundParameters: + """Test suite for Card Set 13 (Background) parameter parsing and formatting. + + Format from Table VI B.2: + Card Set 13 contains various background function definitions. + Currently unimplemented - tests verify NotImplementedError is raised. + """ + + @pytest.fixture + def valid_const_lines(self): + """Sample valid CONST background lines.""" + return [ + "BACKGround functions", + "CONST 1 1.234E+00 2.345E-03 1.000E+01 1.000E+02", + ] + + @pytest.fixture + def valid_const_params(self): + """Sample valid CONST background parameters.""" + return BackgroundParameters( + type=BackgroundType.CONST, + ) + + def test_from_lines_not_implemented(self, valid_const_lines): + """Test that from_lines raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + BackgroundParameters.from_lines(valid_const_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_to_lines_not_implemented(self, valid_const_params): + """Test that to_lines raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + valid_const_params.to_lines() + assert "not yet implemented" in str(exc_info.value) + + @pytest.mark.parametrize("background_type", list(BackgroundType)) + def test_all_types_not_implemented(self, background_type): + """Test NotImplementedError for all background types.""" + params = BackgroundParameters(type=background_type) + with pytest.raises(NotImplementedError) as exc_info: + params.to_lines() + assert "not yet implemented" in str(exc_info.value) + + if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/tests/unit/pleiades/sammy/parameters/test_det_efficiency.py b/tests/unit/pleiades/sammy/parameters/test_det_efficiency.py index 169df0a..d040299 100644 --- a/tests/unit/pleiades/sammy/parameters/test_det_efficiency.py +++ b/tests/unit/pleiades/sammy/parameters/test_det_efficiency.py @@ -1,11 +1,79 @@ #!/usr/bin/env python -"""Tests for the detector efficiency function card. +"""Unit tests for Card Set 15 (Detector Efficiency Parameters) parsing. - -TODO: This test will be implemented in the future when the card is implemented. +Currently tests for NotImplementedError as functionality is not yet implemented. """ import pytest +from pleiades.sammy.parameters.det_efficiency import DetectorEfficiencyParameters + + +class TestDetectorEfficiencyParameters: + """Test suite for Card Set 15 (Detector Efficiency) parameter parsing and formatting. + + Format from Table VI B.2: + Card Set 15 defines detector efficiencies for spin groups: + - Header line "DETECtor efficiencies" + - One or more efficiency definitions + - Each definition includes efficiency value, uncertainty, flag, and group numbers + - Special handling for >29 groups (continued lines) + - Special handling for >99 groups (wider group number fields) + + Currently unimplemented - tests verify NotImplementedError is raised. + """ + + def test_from_lines_basic_not_implemented(self): + """Test that from_lines raises NotImplementedError for basic format.""" + lines = [ + "DETECtor efficiencies", + "1.234E+00 2.345E-03 1 1 2 3", + ] + with pytest.raises(NotImplementedError) as exc_info: + DetectorEfficiencyParameters.from_lines(lines) + assert "not yet implemented" in str(exc_info.value) + + def test_from_lines_multi_not_implemented(self): + """Test that from_lines raises NotImplementedError for multiple groups.""" + lines = [ + "DETECtor efficiencies", + "1.234E+00 2.345E-03 1 1 2 3", + "3.456E+00 4.567E-03 0 4 5 6", + "5.678E+00 6.789E-03 1 7 8 9", + ] + with pytest.raises(NotImplementedError) as exc_info: + DetectorEfficiencyParameters.from_lines(lines) + assert "not yet implemented" in str(exc_info.value) + + def test_from_lines_continuation_not_implemented(self): + """Test that from_lines raises NotImplementedError for continued groups.""" + lines = [ + "DETECtor efficiencies", + "1.234E+00 2.345E-03 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15-1", + "16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", + ] + with pytest.raises(NotImplementedError) as exc_info: + DetectorEfficiencyParameters.from_lines(lines) + assert "not yet implemented" in str(exc_info.value) + + def test_from_lines_wide_format_not_implemented(self): + """Test that from_lines raises NotImplementedError for wide format.""" + lines = [ + "DETECtor efficiencies", + "1.234E+00 2.345E-03 1 1 2 3 4 5-1", + " 6 7 8 9 10", + ] + with pytest.raises(NotImplementedError) as exc_info: + DetectorEfficiencyParameters.from_lines(lines) + assert "not yet implemented" in str(exc_info.value) + + def test_to_lines_not_implemented(self): + """Test that to_lines raises NotImplementedError.""" + params = DetectorEfficiencyParameters() + with pytest.raises(NotImplementedError) as exc_info: + params.to_lines() + assert "not yet implemented" in str(exc_info.value) + + if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/tests/unit/pleiades/sammy/parameters/test_resolution.py b/tests/unit/pleiades/sammy/parameters/test_resolution.py index 5ab40e9..04c8634 100644 --- a/tests/unit/pleiades/sammy/parameters/test_resolution.py +++ b/tests/unit/pleiades/sammy/parameters/test_resolution.py @@ -1,10 +1,119 @@ #!/usr/bin/env python -"""Tests for the resolution function card. +"""Unit tests for Card Set 14 (Resolution Function Parameters) parsing. -TODO: This test will be implemented in the future when the card is implemented. +Currently tests for NotImplementedError as functionality is not yet implemented. """ import pytest +from pleiades.sammy.parameters.resolution import ( + GEELResolutionParameters, + GELINAResolutionParameters, + NTOFResolutionParameters, + ResolutionType, + RPIResolutionParameters, + UserResolutionParameters, +) + + +class TestResolutionParameters: + """Test suite for Card Set 14 (Resolution) parameter parsing and formatting. + + Format from Table VI B.2: + Card Set 14 supports multiple resolution function types each with their own + multi-line parameter structure. + Currently unimplemented - tests verify NotImplementedError is raised. + """ + + @pytest.fixture + def valid_rpi_lines(self): + """Sample valid RPI resolution lines.""" + return [ + "RPI Resolution", + "BURST 1 1.234E+00 2.345E-03", + "TAU 1 1 1 1 1 3.456E+00 4.567E+00 5.678E+00 6.789E+00 7.890E+00", + ] + + @pytest.fixture + def valid_geel_lines(self): + """Sample valid GEEL resolution lines.""" + return [ + "GEEL resolution", + "BURST 1 1.234E+00 2.345E-03", + ] + + @pytest.fixture + def valid_gelina_lines(self): + """Sample valid GELINA resolution lines.""" + return [ + "GELINa resolution", + "BURST 1 1.234E+00 2.345E-03", + ] + + @pytest.fixture + def valid_ntof_lines(self): + """Sample valid NTOF resolution lines.""" + return [ + "NTOF resolution", + "BURST 1 1.234E+00 2.345E-03", + ] + + @pytest.fixture + def valid_user_lines(self): + """Sample valid user-defined resolution lines.""" + return [ + "USER-Defined resolution function", + "FILE=resolution.dat", + ] + + @pytest.fixture + def valid_params_by_type(self): + """Sample valid parameters for each resolution type.""" + return { + ResolutionType.RPI: RPIResolutionParameters(), + ResolutionType.GEEL: GEELResolutionParameters(), + ResolutionType.GELINA: GELINAResolutionParameters(), + ResolutionType.NTOF: NTOFResolutionParameters(), + ResolutionType.USER: UserResolutionParameters(), + } + + def test_rpi_not_implemented(self, valid_rpi_lines): + """Test that RPI resolution parsing raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + RPIResolutionParameters.from_lines(valid_rpi_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_geel_not_implemented(self, valid_geel_lines): + """Test that GEEL resolution parsing raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + GEELResolutionParameters.from_lines(valid_geel_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_gelina_not_implemented(self, valid_gelina_lines): + """Test that GELINA resolution parsing raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + GELINAResolutionParameters.from_lines(valid_gelina_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_ntof_not_implemented(self, valid_ntof_lines): + """Test that NTOF resolution parsing raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + NTOFResolutionParameters.from_lines(valid_ntof_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_user_not_implemented(self, valid_user_lines): + """Test that user-defined resolution parsing raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + UserResolutionParameters.from_lines(valid_user_lines) + assert "not yet implemented" in str(exc_info.value) + + def test_to_lines_not_implemented(self, valid_params_by_type): + """Test that to_lines raises NotImplementedError for all types.""" + for params in valid_params_by_type.values(): + with pytest.raises(NotImplementedError) as exc_info: + params.to_lines() + assert "not yet implemented" in str(exc_info.value) + + if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/tests/unit/pleiades/sammy/parameters/test_user_resolution.py b/tests/unit/pleiades/sammy/parameters/test_user_resolution.py new file mode 100644 index 0000000..32a408b --- /dev/null +++ b/tests/unit/pleiades/sammy/parameters/test_user_resolution.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python +"""Unit tests for Card Set 16 (User-Defined Resolution Function) parsing.""" + +import pytest + +from pleiades.sammy.parameters.helper import VaryFlag +from pleiades.sammy.parameters.user_resolution import UserResolutionParameters + + +class TestUserResolutionParameters: + """Test suite for USER-Defined resolution function parameter parsing.""" + + @pytest.fixture + def valid_header_line(self): + """Sample valid header line.""" + return "USER-Defined resolution function" + + @pytest.fixture + def valid_burst_line(self): + """Sample valid BURST parameter line.""" + return "BURST 1 1.234E+00 2.345E-03" + # | | | | + # | | | 21-30: Uncertainty + # | | 11-20: Burst width + # | 7: Flag + # 1-5: "BURST" + + @pytest.fixture + def valid_channel_line(self): + """Sample valid CHANN parameter line.""" + return "CHANN 1 1.234E+03 2.345E+00 3.456E-03" + # | | | | | + # | | | | 31-40: Uncertainty + # | | | 21-30: Channel width + # | | 11-20: Energy + # | 7: Flag + # 1-5: "CHANN" + + @pytest.fixture + def valid_file_line(self): + """Sample valid FILE parameter line.""" + return "FILE=resolution_data.txt" + # | | + # | 6-75: Filename + # 1-5: "FILE=" + + def test_parse_header_only(self, valid_header_line): + """Test parsing of minimal case with only header line.""" + params = UserResolutionParameters.from_lines([valid_header_line]) + assert params.type == "USER" + assert params.burst_width is None + assert params.burst_uncertainty is None + assert params.burst_flag == VaryFlag.NO + assert params.channel_energies == [] + assert params.channel_widths == [] + assert params.channel_uncertainties == [] + assert params.channel_flags == [] + assert params.filenames == [] + + def test_parse_invalid_header(self): + """Test parsing with invalid header line.""" + with pytest.raises(ValueError, match="Invalid header"): + UserResolutionParameters.from_lines(["WRONG resolution function"]) + + def test_parse_burst_line(self, valid_header_line, valid_burst_line): + """Test parsing of BURST parameter line.""" + params = UserResolutionParameters.from_lines([valid_header_line, valid_burst_line]) + + # Check burst parameters + assert params.burst_width == pytest.approx(1.234) + assert params.burst_uncertainty == pytest.approx(2.345e-3) + assert params.burst_flag == VaryFlag.YES + + # Verify other parameters are empty + assert params.channel_energies == [] + assert params.filenames == [] + + def test_burst_line_formatting(self): + """Test formatting of BURST parameters.""" + params = UserResolutionParameters(burst_width=1.234, burst_uncertainty=2.345e-3, burst_flag=VaryFlag.YES) + + lines = params.to_lines() + assert len(lines) == 3 + assert lines[0] == "USER-Defined resolution function" + + # Check burst line format + burst_line = lines[1] + assert burst_line.startswith("BURST") + assert len(burst_line) >= 30 # At least up to uncertainty field + assert burst_line[6:7] == "1" # Flag value + + # Parse the formatted line to verify values + parsed = UserResolutionParameters.from_lines(lines) + assert parsed.burst_width == pytest.approx(1.234) + assert parsed.burst_uncertainty == pytest.approx(2.345e-3) + assert parsed.burst_flag == VaryFlag.YES + + @pytest.mark.parametrize( + "invalid_line", + [ + "BURST x 1.234E+00 2.345E-03", # Invalid flag + "BURST 1 invalid 2.345E-03", # Invalid width + "BURST", # Incomplete line + "BURTS 1 1.234E+00 2.345E-03", # Wrong identifier + ], + ) + def test_parse_invalid_burst_line(self, valid_header_line, invalid_line): + """Test parsing of invalid BURST parameter lines.""" + with pytest.raises(ValueError): + UserResolutionParameters.from_lines([valid_header_line, invalid_line]) + + def test_parse_single_channel_line(self, valid_header_line, valid_channel_line): + """Test parsing of single CHANN parameter line.""" + params = UserResolutionParameters.from_lines([valid_header_line, valid_channel_line]) + + # Check channel parameters + assert len(params.channel_energies) == 1 + assert len(params.channel_widths) == 1 + assert len(params.channel_uncertainties) == 1 + assert len(params.channel_flags) == 1 + + assert params.channel_energies[0] == pytest.approx(1.234e3) + assert params.channel_widths[0] == pytest.approx(2.345) + assert params.channel_uncertainties[0] == pytest.approx(3.456e-3) + assert params.channel_flags[0] == VaryFlag.YES + + def test_parse_multiple_channel_lines(self, valid_header_line): + """Test parsing of multiple CHANN parameter lines.""" + lines = [ + valid_header_line, + "CHANN 1 1.234E+03 2.345E+00 3.456E-03", + "CHANN 0 4.567E+03 5.678E+00 6.789E-03", + "CHANN 3 7.890E+03 8.901E+00 9.012E-03", + ] + + params = UserResolutionParameters.from_lines(lines) + + # Check all channel parameters are parsed + assert len(params.channel_energies) == 3 + assert len(params.channel_widths) == 3 + assert len(params.channel_uncertainties) == 3 + assert len(params.channel_flags) == 3 + + # Check first channel + assert params.channel_energies[0] == pytest.approx(1.234e3) + assert params.channel_widths[0] == pytest.approx(2.345) + assert params.channel_uncertainties[0] == pytest.approx(3.456e-3) + assert params.channel_flags[0] == VaryFlag.YES + + # Check second channel + assert params.channel_energies[1] == pytest.approx(4.567e3) + assert params.channel_widths[1] == pytest.approx(5.678) + assert params.channel_uncertainties[1] == pytest.approx(6.789e-3) + assert params.channel_flags[1] == VaryFlag.NO + + # Check third channel + assert params.channel_energies[2] == pytest.approx(7.890e3) + assert params.channel_widths[2] == pytest.approx(8.901) + assert params.channel_uncertainties[2] == pytest.approx(9.012e-3) + assert params.channel_flags[2] == VaryFlag.PUP + + def test_channel_line_formatting(self): + """Test formatting of CHANN parameters.""" + params = UserResolutionParameters( + channel_energies=[1.234e3, 4.567e3], + channel_widths=[2.345, 5.678], + channel_uncertainties=[3.456e-3, 6.789e-3], + channel_flags=[VaryFlag.YES, VaryFlag.NO], + ) + + lines = params.to_lines() + assert len(lines) == 4 # Header + 2 channel lines + blank line + assert lines[0] == "USER-Defined resolution function" + + # Check first channel line + assert lines[1].startswith("CHANN") + assert lines[1][6:7] == "1" # Flag value + + # Parse the formatted lines to verify values + parsed = UserResolutionParameters.from_lines(lines) + assert len(parsed.channel_energies) == 2 + assert parsed.channel_energies[0] == pytest.approx(1.234e3) + assert parsed.channel_widths[0] == pytest.approx(2.345) + assert parsed.channel_uncertainties[0] == pytest.approx(3.456e-3) + assert parsed.channel_flags[0] == VaryFlag.YES + + @pytest.mark.parametrize( + "invalid_line", + [ + "CHANN x 1.234E+03 2.345E+00 3.456E-03", # Invalid flag + "CHANN 1 invalid 2.345E+00 3.456E-03", # Invalid energy + "CHANN 1 1.234E+03 invalid 3.456E-03", # Invalid width + "CHANN", # Incomplete line + "CHANL 1 1.234E+03 2.345E+00 3.456E-03", # Wrong identifier + ], + ) + def test_parse_invalid_channel_line(self, valid_header_line, invalid_line): + """Test parsing of invalid CHANN parameter lines.""" + with pytest.raises(ValueError): + UserResolutionParameters.from_lines([valid_header_line, invalid_line]) + + def test_parse_single_file_line(self, valid_header_line, valid_file_line): + """Test parsing of single FILE parameter line.""" + params = UserResolutionParameters.from_lines([valid_header_line, valid_file_line]) + + # Check file parameters + assert len(params.filenames) == 1 + assert params.filenames[0] == "resolution_data.txt" + + def test_parse_multiple_file_lines(self, valid_header_line): + """Test parsing of multiple FILE parameter lines.""" + lines = [valid_header_line, "FILE=resolution_data1.txt", "FILE=resolution_data2.txt", "FILE=resolution_data3.txt"] + + params = UserResolutionParameters.from_lines(lines) + + # Check all files are parsed + assert len(params.filenames) == 3 + assert params.filenames[0] == "resolution_data1.txt" + assert params.filenames[1] == "resolution_data2.txt" + assert params.filenames[2] == "resolution_data3.txt" + + def test_file_line_formatting(self): + """Test formatting of FILE parameters.""" + params = UserResolutionParameters(filenames=["resolution_data1.txt", "resolution_data2.txt"]) + + lines = params.to_lines() + assert len(lines) == 4 # Header + 2 file lines + blank line + assert lines[0] == "USER-Defined resolution function" + + # Check file lines + assert lines[1] == "FILE=resolution_data1.txt" + assert lines[2] == "FILE=resolution_data2.txt" + + # Parse the formatted lines to verify values + parsed = UserResolutionParameters.from_lines(lines) + assert len(parsed.filenames) == 2 + assert parsed.filenames[0] == "resolution_data1.txt" + assert parsed.filenames[1] == "resolution_data2.txt" + + @pytest.mark.parametrize( + "invalid_line", + [ + "FILE", # Incomplete line + "FILE resolution_data.txt", # Missing equals sign + "FILES=resolution_data.txt", # Wrong identifier + "FILE=", # Missing filename + ], + ) + def test_parse_invalid_file_line(self, valid_header_line, invalid_line): + """Test parsing of invalid FILE parameter lines.""" + with pytest.raises(ValueError): + UserResolutionParameters.from_lines([valid_header_line, invalid_line]) + + def test_complete_parameter_set(self, valid_header_line, valid_burst_line, valid_channel_line, valid_file_line): + """Test parsing of complete parameter set with all optional sections.""" + lines = [valid_header_line, valid_burst_line, valid_channel_line, valid_file_line] + + params = UserResolutionParameters.from_lines(lines) + + # Check burst parameters + assert params.burst_width == pytest.approx(1.234) + assert params.burst_uncertainty == pytest.approx(2.345e-3) + assert params.burst_flag == VaryFlag.YES + + # Check channel parameters + assert len(params.channel_energies) == 1 + assert params.channel_energies[0] == pytest.approx(1.234e3) + assert params.channel_widths[0] == pytest.approx(2.345) + assert params.channel_uncertainties[0] == pytest.approx(3.456e-3) + assert params.channel_flags[0] == VaryFlag.YES + + # Check file parameters + assert len(params.filenames) == 1 + assert params.filenames[0] == "resolution_data.txt" + + def test_blank_line_at_end(self): + """Test that formatted output includes blank line at end per spec.""" + params = UserResolutionParameters(filenames=["test.dat"]) + + lines = params.to_lines() + assert len(lines) >= 2 # At least header, content, and blank line + assert lines[-1] == "" # Last line should be blank + + def test_mixed_section_order(self): + """Test that sections can appear in any order.""" + lines = [ + "USER-Defined resolution function", + "FILE=data1.txt", + "CHANN 1 1.000E+03 2.000E+00 3.000E-03", + "BURST 1 1.234E+00 2.345E-03", + "CHANN 0 2.000E+03 3.000E+00 4.000E-03", + "FILE=data2.txt", + "", # blank line + ] + + params = UserResolutionParameters.from_lines(lines) + + # Verify all sections parsed correctly regardless of order + assert len(params.channel_energies) == 2 + assert len(params.filenames) == 2 + assert params.burst_width is not None + + # Verify specific values + assert params.channel_energies[0] == pytest.approx(1000.0) + assert params.channel_energies[1] == pytest.approx(2000.0) + assert params.filenames == ["data1.txt", "data2.txt"] + assert params.burst_width == pytest.approx(1.234) + + def test_maximum_filename_length(self): + """Test that filename field respects column limit.""" + # Generate filename that's too long (>70 characters) + long_filename = "x" * 71 + + with pytest.raises(ValueError, match="exceeds maximum length"): + UserResolutionParameters.from_lines(["USER-Defined resolution function", f"FILE={long_filename}", ""]) + + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/tests/unit/pleiades/sammy/test_config.py b/tests/unit/pleiades/sammy/test_config.py index 516aa6f..0478e84 100644 --- a/tests/unit/pleiades/sammy/test_config.py +++ b/tests/unit/pleiades/sammy/test_config.py @@ -24,9 +24,7 @@ def test_create_with_valid_paths(self, temp_working_dir): def test_create_with_defaults(self, temp_working_dir): """Should create config with default values.""" - config = LocalSammyConfig( - working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy") - ) + config = LocalSammyConfig(working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy")) assert config.shell_path == Path("/bin/bash") def test_validate_sammy_in_path(self, temp_working_dir, monkeypatch): @@ -37,18 +35,14 @@ def mock_which(path): monkeypatch.setattr("shutil.which", mock_which) - config = LocalSammyConfig( - working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy") - ) + config = LocalSammyConfig(working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy")) assert config.validate() def test_validate_sammy_not_in_path(self, temp_working_dir, monkeypatch): """Should raise error if SAMMY not in PATH.""" monkeypatch.setattr("shutil.which", lambda _: None) - config = LocalSammyConfig( - working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy") - ) + config = LocalSammyConfig(working_dir=temp_working_dir, output_dir=temp_working_dir / "output", sammy_executable=Path("sammy")) with pytest.raises(ConfigurationError) as exc: config.validate() assert "not found" in str(exc.value) @@ -171,9 +165,7 @@ def test_validate_invalid_url(self, temp_working_dir): def test_empty_url(self, temp_working_dir): """Should raise error for empty URL.""" - config = NovaSammyConfig( - working_dir=temp_working_dir, output_dir=temp_working_dir / "output", url="", api_key="valid_api_key" - ) + config = NovaSammyConfig(working_dir=temp_working_dir, output_dir=temp_working_dir / "output", url="", api_key="valid_api_key") with pytest.raises(ConfigurationError) as exc: config.validate() assert "NOVA service URL cannot be empty" in str(exc.value) diff --git a/tests/unit/pleiades/sammy/test_factory.py b/tests/unit/pleiades/sammy/test_factory.py index 6ca5a0d..2898c25 100644 --- a/tests/unit/pleiades/sammy/test_factory.py +++ b/tests/unit/pleiades/sammy/test_factory.py @@ -73,9 +73,7 @@ def _mock_run(*args, **kwargs): @pytest.fixture def mock_subprocess_run_docker_fail(monkeypatch): """Mock subprocess.run to simulate docker info failure.""" - monkeypatch.setattr( - subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args=args, returncode=1, **kwargs) - ) + monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args=args, returncode=1, **kwargs)) # Mock SammyRunner creation for runner tests @@ -100,9 +98,7 @@ def test_list_available_backends_all_available(self, mock_which, mock_subprocess BackendType.NOVA: True, } - def test_list_available_backends_none_available( - self, monkeypatch, mock_which_unavailable, mock_subprocess_run_docker_fail - ): + def test_list_available_backends_none_available(self, monkeypatch, mock_which_unavailable, mock_subprocess_run_docker_fail): """No backends should be available.""" _ = mock_which_unavailable, mock_subprocess_run_docker_fail # implicitly used by the fixture # remove NOVA env vars to simulate unavailability @@ -218,9 +214,7 @@ def test_auto_select_local(self, mock_which, mock_subprocess_run, mock_sammy_run assert isinstance(runner, LocalSammyRunner) assert "Attempting to use local backend" in caplog.text - def test_auto_select_docker( - self, monkeypatch, mock_which_unavailable, mock_subprocess_run, mock_sammy_runner, tmp_path, caplog - ): + def test_auto_select_docker(self, monkeypatch, mock_which_unavailable, mock_subprocess_run, mock_sammy_runner, tmp_path, caplog): """Should auto-select docker backend if local unavailable.""" _ = mock_which_unavailable, mock_subprocess_run, mock_sammy_runner # implicitly used by the fixture # remove NOVA env vars to simulate unavailability @@ -240,9 +234,7 @@ def test_auto_select_preferred(self, mock_which, mock_subprocess_run, mock_sammy assert isinstance(runner, DockerSammyRunner) assert "Using preferred backend: docker" in caplog.text - def test_auto_select_none_available( - self, monkeypatch, mock_which_unavailable, mock_subprocess_run_docker_fail, tmp_path, caplog - ): + def test_auto_select_none_available(self, monkeypatch, mock_which_unavailable, mock_subprocess_run_docker_fail, tmp_path, caplog): """Should raise error if no backends available.""" _ = mock_which_unavailable, mock_subprocess_run_docker_fail # implicitly used by the fixture # remove NOVA env vars to simulate unavailability