feat(cli): separate-branches rename, short aliases, clean dry-run
- --normalize-to-one renamed to --separate-branches (no alias; the name now describes the output choice: total spectrum with intensity-normalized branches vs separate branches, each normalized to unit area with intensities left to the data user). SpectrumConfig.normalize_to_one -> separate_branches, CSV header word to_one -> separate. - every long option gains a single-letter alias: -N -i -s -m -o -p -L -d -c -V -S -b (only -v/-q existed before) - dry-run drops the top-level Transition: line — the per-branch list below already reports each branch's transition type - version bumped to 0.4.1 (pyproject, __version__, CSV semantics docstring, TODO roadmap entry)
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""CLI option surface tests: renamed branch-separation flag, short
|
||||
aliases for every long option, and dry-run output shape (audit follow-up
|
||||
by the maintainer: rename --normalize-to-one, add short options, drop the
|
||||
redundant top-level Transition line)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from beta_spectrum import BranchConfig, SpectrumConfig
|
||||
from beta_spectrum.cli import _build_parser, _print_dry_run_output
|
||||
|
||||
pytestmark = pytest.mark.quality
|
||||
|
||||
|
||||
def make_config() -> SpectrumConfig:
|
||||
return SpectrumConfig(
|
||||
Z_parent=59,
|
||||
Z_daughter=60,
|
||||
A_number=144,
|
||||
endpoint_MeV=2.98,
|
||||
transition_type="F1",
|
||||
e_step_MeV=0.001,
|
||||
branches=[
|
||||
BranchConfig(endpoint_MeV=2.98, transition_type="F1", intensity=0.9),
|
||||
BranchConfig(endpoint_MeV=2.1, transition_type="A", intensity=0.1),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestSeparateBranchesRename:
|
||||
"""--normalize-to-one was renamed to --separate-branches (no alias)."""
|
||||
|
||||
def test_flag_parses_true(self) -> None:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(["input.json", "--separate-branches"])
|
||||
assert args.separate_branches is True
|
||||
|
||||
def test_flag_default_is_none(self) -> None:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(["input.json"])
|
||||
assert args.separate_branches is None
|
||||
|
||||
def test_old_flag_is_rejected(self) -> None:
|
||||
parser = _build_parser()
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["input.json", "--normalize-to-one"])
|
||||
|
||||
def test_config_field_renamed(self) -> None:
|
||||
names = {f.name for f in dataclasses.fields(SpectrumConfig)}
|
||||
assert "separate_branches" in names
|
||||
assert "normalize_to_one" not in names
|
||||
|
||||
def test_config_old_kwarg_rejected(self) -> None:
|
||||
kwargs: dict[str, Any] = {
|
||||
"Z_parent": 15,
|
||||
"Z_daughter": 16,
|
||||
"A_number": 32,
|
||||
"endpoint_MeV": 1.7103,
|
||||
"transition_type": "A",
|
||||
"normalize_to_one": True,
|
||||
}
|
||||
with pytest.raises(TypeError):
|
||||
SpectrumConfig(**kwargs)
|
||||
|
||||
|
||||
# (short, long, value-or-None-for-store_true)
|
||||
_SHORT_EQUIV = [
|
||||
("-N", "--nuclide", "Cs137"),
|
||||
("-i", "--input", "in.json"),
|
||||
("-s", "--e-step", "0.01"),
|
||||
("-m", "--neutrino-mass", "0.15"),
|
||||
("-o", "--output", "out.csv"),
|
||||
("-p", "--plot", "plot.png"),
|
||||
("-L", "--log-file", "calc.log"),
|
||||
("-d", "--dry-run", None),
|
||||
("-c", "--intensity-cutoff", "0.5"),
|
||||
("-V", "--csv-verbosity", "concise"),
|
||||
("-S", "--csv-separator", "tab"),
|
||||
("-b", "--separate-branches", None),
|
||||
]
|
||||
|
||||
|
||||
class TestShortOptions:
|
||||
"""Every long option gains a single-letter alias (only -v/-q existed)."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"short,long,value",
|
||||
_SHORT_EQUIV,
|
||||
ids=[s.lstrip("-") for s, _, _ in _SHORT_EQUIV],
|
||||
)
|
||||
def test_short_matches_long(
|
||||
self, short: str, long: str, value: Optional[str]
|
||||
) -> None:
|
||||
parser = _build_parser()
|
||||
argv_long = [long] if value is None else [long, value]
|
||||
argv_short = [short] if value is None else [short, value]
|
||||
args_long = parser.parse_args(["Tc99"] + argv_long)
|
||||
args_short = parser.parse_args(["Tc99"] + argv_short)
|
||||
dest = long.lstrip("-").replace("-", "_")
|
||||
assert getattr(args_short, dest) == getattr(args_long, dest)
|
||||
assert getattr(args_long, dest) is not None or value is None
|
||||
|
||||
def test_existing_shorts_unchanged(self) -> None:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(["Tc99", "-v", "-q"])
|
||||
assert args.verbose == 1
|
||||
assert args.quiet is True
|
||||
|
||||
|
||||
class TestDryRunOutputShape:
|
||||
"""The top-level Transition line is redundant with the per-branch
|
||||
list (for multi-branch runs) and was removed."""
|
||||
|
||||
def test_multi_branch_dry_run_no_top_level_transition(self, capsys: Any) -> None:
|
||||
_print_dry_run_output(make_config(), "paceENSDF")
|
||||
out = capsys.readouterr().out
|
||||
assert "Transition:" not in out
|
||||
assert "transition=F1" in out
|
||||
assert "transition=A" in out
|
||||
assert "Multi-branch mode: 2 branches" in out
|
||||
|
||||
def test_single_branch_dry_run_reports_endpoint(self, capsys: Any) -> None:
|
||||
config = SpectrumConfig(
|
||||
Z_parent=15,
|
||||
Z_daughter=16,
|
||||
A_number=32,
|
||||
endpoint_MeV=1.7103,
|
||||
transition_type="A",
|
||||
)
|
||||
_print_dry_run_output(config, "json")
|
||||
out = capsys.readouterr().out
|
||||
assert "Transition:" not in out
|
||||
assert "Mode: single-branch" in out
|
||||
Reference in New Issue
Block a user