diff --git a/ccp4.xml b/ccp4.xml index 1254b401dbe216b423e8bd6b29323e2177f15e24..57d11d40eba8349bc748e842c3bb9b8c3583ffc4 100644 --- a/ccp4.xml +++ b/ccp4.xml @@ -1701,6 +1701,8 @@ <patch file="cctbx-auto.patch" strip="0" /> <patch file="xia2-Handlers-CommandLine.patch" strip="0" /> <!-- nproc on windows --> <patch file="xia2-Modules-wait-for-completion.patch" strip="0" /> + <patch file="xia2-ssx-windows.patch" strip="0" /> + <patch file="xia2-tests.patch" strip="0" /> </branch> <dependencies> <dep package="cctbx-packages"/> diff --git a/patches/xia2-ssx-windows.patch b/patches/xia2-ssx-windows.patch new file mode 100644 index 0000000000000000000000000000000000000000..35c1f4fbbd5dd75452e81a2181fbff5a5defaaf7 --- /dev/null +++ b/patches/xia2-ssx-windows.patch @@ -0,0 +1,64 @@ +diff --git a/src/xia2/Modules/SSX/data_integration_standard.py b/src/xia2/Modules/SSX/data_integration_standard.py +index f78a1cdc..d3f4a196 100644 +--- xia2/src/xia2/Modules/SSX/data_integration_standard.py ++++ xia2/src/xia2/Modules/SSX/data_integration_standard.py +@@ -289,14 +289,12 @@ def _handle_slices(images_or_templates, path_type="image"): + import_command = [] + for obj in images_or_templates: + # here we only care about ':' which are later than C:\ +- if ":" in obj: +- tokens = obj.split(":") +- # cope with windows drives i.e. C:\data\blah\thing_0001.cbf:1:100 +- if len(tokens[0]) == 1: +- tokens = [f"{tokens[0]}:{tokens[1]}"] + tokens[2:] ++ drive, tail = os.path.splitdrive(obj) ++ if ":" in tail: ++ tokens = tail.split(":") + if len(tokens) != 3: + raise RuntimeError("/path/to/image.h5:start:end") +- dataset = tokens[0] ++ dataset = drive + tokens[0] + starts.append(int(tokens[1])) + ends.append(int(tokens[2])) + import_command.append( +@@ -339,8 +337,11 @@ def run_import( + pathlib.Path.mkdir(working_directory) + + xia2_logger.info("New images or geometry detected, running import") ++ cmd = "dials.import" ++ if os.name == "nt": ++ cmd += ".bat" + import_command = [ +- "dials.import", ++ cmd, + "output.experiments=imported.expt", + "convert_stills_to_sequences=True", + ] +@@ -655,9 +656,12 @@ def determine_reference_geometry_from_images( + xia2_logger.info( + f"Refined reference geometry saved to {working_directory}/refined.expt" + ) ++ cmd = "dxtbx.plot_detector_models" ++ if os.name == "nt": ++ cmd += ".bat" + subprocess.run( + [ +- "dxtbx.plot_detector_models", ++ cmd, + "imported.expt", + "refined.expt", + "pdf_file=detector_models.pdf", +@@ -758,9 +762,12 @@ def cumulative_determine_reference_geometry( + xia2_logger.info( + f"Refined reference geometry saved to {working_directory}/refined.expt" + ) ++ cmd = "dxtbx.plot_detector_models" ++ if os.name == "nt": ++ cmd += ".bat" + subprocess.run( + [ +- "dxtbx.plot_detector_models", ++ cmd, + "imported.expt", + "refined.expt", + "pdf_file=detector_models.pdf", diff --git a/patches/xia2-tests.patch b/patches/xia2-tests.patch new file mode 100644 index 0000000000000000000000000000000000000000..bd4f442d5031ba6cacba0cec79b6b95a2c575b22 --- /dev/null +++ b/patches/xia2-tests.patch @@ -0,0 +1,843 @@ +diff --git a/tests/Applications/test_xia2setup.py b/tests/Applications/test_xia2setup.py +index 55650549..e7a1ad0d 100644 +--- xia2/tests/Applications/test_xia2setup.py ++++ xia2/tests/Applications/test_xia2setup.py +@@ -1,6 +1,7 @@ + from __future__ import annotations + + import os ++import shutil + import subprocess + + import pytest +@@ -13,19 +14,27 @@ def insulin_with_missing_image(dials_data, tmp_path): + for j in range(1, 46): + if j == 23: + continue +- tmp_path.joinpath(f"insulin_1_{j:03d}.img").symlink_to( +- dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img" +- ) ++ try: ++ tmp_path.joinpath(f"insulin_1_{j:03d}.img").symlink_to( ++ dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img" ++ ) ++ except OSError: ++ shutil.copy( ++ dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img", tmp_path ++ ) + return tmp_path / "insulin_1_###.img" + + + def test_write_xinfo_insulin_with_missing_image(insulin_with_missing_image, tmp_path): ++ cmd = "xia2.setup" ++ if os.name == "nt": ++ cmd += ".exe" + result = subprocess.run( + [ +- "xia2.setup", ++ cmd, + f"image={insulin_with_missing_image.parent.joinpath('insulin_1_001.img')}", + ], +- env={"CCP4": tmp_path, **os.environ}, ++ env={"CCP4": str(tmp_path), **os.environ}, + cwd=tmp_path, + ) + assert not result.returncode +@@ -39,13 +48,16 @@ def test_write_xinfo_insulin_with_missing_image(insulin_with_missing_image, tmp_ + + + def test_write_xinfo_template_missing_images(insulin_with_missing_image, tmp_path): ++ cmd = "xia2.setup" ++ if os.name == "nt": ++ cmd += ".exe" + result = subprocess.run( + [ +- "xia2.setup", ++ cmd, + f"image={insulin_with_missing_image.parent.joinpath('insulin_1_001.img:1:22')}", + "read_all_image_headers=False", + ], +- env={"CCP4": tmp_path, **os.environ}, ++ env={"CCP4": str(tmp_path), **os.environ}, + cwd=tmp_path, + ) + assert not result.returncode +@@ -58,14 +70,17 @@ def test_write_xinfo_template_missing_images(insulin_with_missing_image, tmp_pat + + + def test_write_xinfo_split_sweep(dials_data, tmp_path): ++ cmd = "xia2.setup" ++ if os.name == "nt": ++ cmd += ".exe" + result = subprocess.run( + [ +- "xia2.setup", ++ cmd, + f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:1:22'}", + f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:23:45'}", + "read_all_image_headers=False", + ], +- env={"CCP4": tmp_path, **os.environ}, ++ env={"CCP4": str(tmp_path), **os.environ}, + cwd=tmp_path, + ) + assert not result.returncode +@@ -80,13 +95,16 @@ def test_write_xinfo_split_sweep(dials_data, tmp_path): + + def test_write_xinfo_unroll(dials_data, tmp_path): + # This test partially exercises the fix to https://github.com/xia2/xia2/issues/498 with a different syntax ++ cmd = "xia2.setup" ++ if os.name == "nt": ++ cmd += ".exe" + result = subprocess.run( + [ +- "xia2.setup", ++ cmd, + f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:1:45:15'}", + "read_all_image_headers=False", + ], +- env={"CCP4": tmp_path, **os.environ}, ++ env={"CCP4": str(tmp_path), **os.environ}, + cwd=tmp_path, + ) + assert not result.returncode +diff --git a/tests/command_line/test_to_shelx.py b/tests/command_line/test_to_shelx.py +index 2b2f6c51..40dcc6a1 100644 +--- xia2/tests/command_line/test_to_shelx.py ++++ xia2/tests/command_line/test_to_shelx.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import os + import subprocess + + +@@ -9,13 +10,17 @@ def test_to_shelx(dials_data, tmp_path): + # First create an unmerged mtz. + expt = l_cyst / "scaled_30.expt" + refls = l_cyst / "scaled_30.refl" +- result = subprocess.run( +- ["dials.export", expt, refls, "mtz.hklout=scaled.mtz"], cwd=tmp_path +- ) ++ cmd = "dials.export" ++ if os.name == "nt": ++ cmd += ".bat" ++ result = subprocess.run([cmd, expt, refls, "mtz.hklout=scaled.mtz"], cwd=tmp_path) + assert not result.returncode or result.stderr + + # now test the program +- args = ["xia2.to_shelx", tmp_path / "scaled.mtz", "lcys", "C3H7NO2S"] ++ cmd = "xia2.to_shelx" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, tmp_path / "scaled.mtz", "lcys", "C3H7NO2S"] + result = subprocess.run(args, cwd=tmp_path) + assert not result.returncode or result.stderr + assert (tmp_path / "lcys.hkl").is_file() +@@ -23,7 +28,7 @@ def test_to_shelx(dials_data, tmp_path): + + # now test the program with '--cell' option + args = [ +- "xia2.to_shelx", ++ cmd, + tmp_path / "scaled.mtz", + "lcyst", + "C3H7NO2S", +diff --git a/tests/regression/test_X4_wide.py b/tests/regression/test_X4_wide.py +index cadccaf9..4a157b0f 100644 +--- xia2/tests/regression/test_X4_wide.py ++++ xia2/tests/regression/test_X4_wide.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import os + import subprocess + + import pytest +@@ -47,12 +48,14 @@ END PROJECT AUTOMATIC + + @pytest.mark.parametrize("pipeline,scaler", (("dials", "xdsa"), ("3dii", "dials"))) + def test_incompatible_pipeline_scaler(pipeline, scaler, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + result = subprocess.run( +- ["xia2", f"pipeline={pipeline}", "nproc=1", f"scaler={scaler}"], ++ [cmd, f"pipeline={pipeline}", "nproc=1", f"scaler={scaler}"], + cwd=tmp_path, + capture_output=True, + ) +- assert result.returncode + assert ( + f"Error: scaler={scaler} not compatible with pipeline={pipeline}" + in result.stdout.decode("latin-1") +@@ -60,8 +64,11 @@ def test_incompatible_pipeline_scaler(pipeline, scaler, tmp_path, ccp4): + + + def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".ese" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials-aimless", + "nproc=1", + "trust_beam_centre=True", +@@ -78,8 +85,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): + + def test_dials_aimless_with_dials_pipeline(regression_test, dials_data, tmp_path, ccp4): + # This should be functionally equivalent to 'test_dials_aimless' above ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials", + "scaler=ccp4a", + "nproc=1", +@@ -96,8 +106,11 @@ def test_dials_aimless_with_dials_pipeline(regression_test, dials_data, tmp_path + + + def test_dials(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials", + "nproc=1", + "trust_beam_centre=True", +@@ -146,8 +159,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4): + + + def test_dials_aimless_split(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials-aimless", + "nproc=1", + "njob=2", +@@ -163,8 +179,11 @@ def test_dials_aimless_split(regression_test, dials_data, tmp_path, ccp4): + + + def test_dials_split(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials", + "nproc=1", + "njob=2", +@@ -187,8 +206,11 @@ def test_dials_split(regression_test, dials_data, tmp_path, ccp4): + + + def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "trust_beam_centre=True", +@@ -203,8 +225,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): + + + def test_xds_split(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "njob=2", +@@ -220,8 +245,11 @@ def test_xds_split(regression_test, dials_data, tmp_path, ccp4, xds): + + + def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "scaler=ccp4a", +@@ -236,8 +264,11 @@ def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds): + + + def test_xds_ccp4a_split(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "scaler=ccp4a", +@@ -259,8 +290,11 @@ def test_xds_ccp4a_split(regression_test, dials_data, tmp_path, ccp4, xds): + def test_space_group_dials( + pipeline, space_group, regression_test, dials_data, tmp_path, ccp4 + ): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=%s" % pipeline, + f"space_group={space_group}", + "nproc=1", +@@ -286,8 +320,11 @@ def test_space_group_dials( + def test_space_group_3dii( + space_group, regression_test, dials_data, tmp_path, ccp4, xds + ): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3dii", + f"space_group={space_group}", + "nproc=1", +diff --git a/tests/regression/test_cluster_analysis.py b/tests/regression/test_cluster_analysis.py +index d9844288..5a155a77 100644 +--- xia2/tests/regression/test_cluster_analysis.py ++++ xia2/tests/regression/test_cluster_analysis.py +@@ -97,11 +97,17 @@ def test_serial_data(dials_data, tmp_path, run_cluster_identification): + ssx = dials_data("cunir_serial_processed", pathlib=True) + expt_int = os.fspath(ssx / "integrated.expt") + refl_int = os.fspath(ssx / "integrated.refl") +- args_generate_scaled = ["xia2.ssx_reduce", expt_int, refl_int] ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args_generate_scaled = [cmd, expt_int, refl_int] + expt_scaled = os.fspath(tmp_path / "DataFiles" / "scaled.expt") + refl_scaled = os.fspath(tmp_path / "DataFiles" / "scaled.refl") ++ cmd = "xia2.cluster_analysis" ++ if os.name == "nt": ++ cmd += ".exe" + args_test_clustering = [ +- "xia2.cluster_analysis", ++ cmd, + "min_cluster_size=2", + expt_scaled, + refl_scaled, +@@ -140,8 +146,11 @@ def test_rotation_data(dials_data, run_in_tmp_path): + refl_4, + ] + ) ++ cmd = "xia2.cluster_analysis" ++ if os.name == "nt": ++ cmd += ".exe" + args_clustering = [ +- "xia2.cluster_analysis", ++ cmd, + "min_cluster_size=2", + expt_scaled, + refl_scaled, +diff --git a/tests/regression/test_mad_example.py b/tests/regression/test_mad_example.py +index cfe7de36..64ec3827 100644 +--- xia2/tests/regression/test_mad_example.py ++++ xia2/tests/regression/test_mad_example.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import os + import subprocess + + import xia2.Test.regression +@@ -16,8 +17,11 @@ expected_data_files = [ + + + def test_dials(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials", + "nproc=1", + "njob=2", +@@ -37,8 +41,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4): + + + def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials-aimless", + "nproc=1", + "njob=2", +@@ -58,8 +65,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): + + + def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "njob=2", +@@ -80,8 +90,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): + + + def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3di", + "nproc=1", + "njob=2", +diff --git a/tests/regression/test_multiple_sweeps.py b/tests/regression/test_multiple_sweeps.py +index 31bb279a..45c6ad31 100644 +--- xia2/tests/regression/test_multiple_sweeps.py ++++ xia2/tests/regression/test_multiple_sweeps.py +@@ -10,6 +10,7 @@ Test the behaviour of the `multiple_sweep_indexing`, `multiple_sweep_refinement` + + from __future__ import annotations + ++import os + import subprocess + + import pytest +@@ -38,9 +39,12 @@ def test_multiple_sweeps(multi_sweep_type, ccp4, dials_data, tmp_path): + data_dir = dials_data("l_cysteine_dials_output", pathlib=True) + images = [data_dir / f"l-cyst_{sweep:02d}_00001.cbf:1:15" for sweep in (1, 2)] + ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command = [ + # Obviously, we're going to run xia2. +- "xia2", ++ cmd, + # Set one of the multiple-sweep flags. + f"{multi_sweep_type}=True", + # Reduce the required number of reflections per degree for profile modelling +diff --git a/tests/regression/test_overload.py b/tests/regression/test_overload.py +index 9b7ba9db..35c6485d 100644 +--- xia2/tests/regression/test_overload.py ++++ xia2/tests/regression/test_overload.py +@@ -1,13 +1,15 @@ + from __future__ import annotations + ++import os + import subprocess + + + def test(dials_data, tmp_path): + images = list(dials_data("centroid_test_data", pathlib=True).glob("centroid*.cbf")) + +- result = subprocess.run( +- ["xia2.overload"] + images, cwd=tmp_path, capture_output=True +- ) ++ cmd = "xia2.overload" ++ if os.name == "nt": ++ cmd += ".exe" ++ result = subprocess.run([cmd] + images, cwd=tmp_path, capture_output=True) + assert not result.returncode and not result.stderr + assert (tmp_path / "overload.json").is_file() +diff --git a/tests/regression/test_small_molecule.py b/tests/regression/test_small_molecule.py +index ebe04098..2db7ea36 100644 +--- xia2/tests/regression/test_small_molecule.py ++++ xia2/tests/regression/test_small_molecule.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import os + import subprocess + + import xia2.Test.regression +@@ -13,8 +14,11 @@ expected_data_files = [ + + + def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials-aimless", + "nproc=2", + "small_molecule=True", +@@ -34,8 +38,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4): + + + def test_dials(regression_test, dials_data, tmp_path, ccp4): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=dials", + "nproc=2", + "small_molecule=True", +@@ -55,8 +62,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4): + + + def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3dii", + "nproc=2", + "small_molecule=True", +@@ -77,8 +87,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds): + + + def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds): ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + "pipeline=3dii", + "nproc=2", + "small_molecule=True", +diff --git a/tests/regression/test_ssx.py b/tests/regression/test_ssx.py +index 77c1a927..1158e75f 100644 +--- xia2/tests/regression/test_ssx.py ++++ xia2/tests/regression/test_ssx.py +@@ -49,7 +49,10 @@ def test_assess_crystals(dials_data, tmp_path, option, expected_success): + # due to very thin batch size. + with (tmp_path / "index.phil").open(mode="w") as f: + f.write("indexing.max_cell=150") +- args = ["xia2.ssx", option, "indexing.phil=index.phil"] ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, option, "indexing.phil=index.phil"] + args.append("image=" + os.fspath(ssx / "merlin0047_1700*.cbf")) + + result = subprocess.run(args, cwd=tmp_path, capture_output=True) +@@ -81,8 +84,11 @@ def test_import_phil_handling(dials_data, tmp_path): + f.write("geometry.beam.wavelength=1.36\ngeometry.detector.distance=247.6") + with (tmp_path / "index.phil").open(mode="w") as f: + f.write("indexing.max_cell=150") ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "steps=None", + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", +@@ -126,8 +132,11 @@ def test_geometry_refinement(dials_data, tmp_path, option, expected_success): + # due to very thin batch size. + with (tmp_path / "index.phil").open(mode="w") as f: + f.write("indexing.max_cell=150") ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "steps=None", + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", +@@ -175,8 +184,11 @@ def test_geometry_refinement(dials_data, tmp_path, option, expected_success): + def refined_expt(dials_data, tmp_path): + ssx = dials_data("cunir_serial", pathlib=True) + ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "steps=None", + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", +@@ -208,8 +220,11 @@ def test_run_with_reference(dials_data, tmp_path, refined_expt, starting): + + ssx = dials_data("cunir_serial", pathlib=True) + ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", + "integration.algorithm=stills", +@@ -241,8 +256,11 @@ def test_slice_cbfs(dials_data, tmp_path, refined_expt): + + ssx = dials_data("cunir_serial", pathlib=True) + ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", + "integration.algorithm=stills", +@@ -280,8 +298,11 @@ def test_slice_cbfs(dials_data, tmp_path, refined_expt): + def test_full_run_without_reference(dials_data, tmp_path): + ssx = dials_data("cunir_serial", pathlib=True) + ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", + "integration.algorithm=stills", +@@ -334,8 +355,11 @@ def test_full_run_without_reference(dials_data, tmp_path): + def test_stepwise_run_without_reference(dials_data, tmp_path): + ssx = dials_data("cunir_serial", pathlib=True) + ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + "unit_cell=96.4,96.4,96.4,90,90,90", + "space_group=P213", + "integration.algorithm=stills", +@@ -455,9 +479,12 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity): + ssx = dials_data("cunir_serial_processed", pathlib=True) + if not idx_ambiguity: + # Reindex to P432, which doesn't have an indexing ambiguity. ++ cmd = "dials.reindex" ++ if os.name == "nt": ++ cmd += ".bat" + result = subprocess.run( + [ +- "dials.reindex", ++ cmd, + f"{ssx / 'integrated.refl'}", + f"{ssx / 'integrated.expt'}", + "space_group=P432", +@@ -466,16 +493,22 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity): + capture_output=True, + ) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + expts = tmp_path / "reindexed.expt" + refls = tmp_path / "reindexed.refl" ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx_reduce", ++ cmd, + f"{refls}", + f"{expts}", + ] # note - pass as files rather than directory to test that input option + else: +- args = ["xia2.ssx_reduce", f"directory={ssx}", "batch_size=2"] ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, f"directory={ssx}", "batch_size=2"] + extra_args = [] + if pdb_model: + model = dials_data("cunir_serial", pathlib=True) / "2BW4.pdb" +@@ -492,14 +525,17 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity): + + result = subprocess.run(args + extra_args, cwd=tmp_path, capture_output=True) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + check_data_reduction_files(tmp_path, reference=pdb_model, reindex=idx_ambiguity) + + # now run again only on previously scaled data + pathlib.Path.mkdir(tmp_path / "reduce") ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" + args = ( + [ +- "xia2.ssx_reduce", ++ cmd, + "steps=merge", + ] + + list((tmp_path / "DataFiles").glob("scale*")) +@@ -507,7 +543,7 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity): + ) + result = subprocess.run(args, cwd=tmp_path / "reduce", capture_output=True) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + check_data_reduction_files_on_scaled_only(tmp_path / "reduce", reference=pdb_model) + + +@@ -534,10 +570,13 @@ grouping: + with open(tmp_path / "example.yaml", "w") as f: + f.write(grouping_yml) + +- args = ["xia2.ssx_reduce", "grouping=example.yaml"] + files ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, "grouping=example.yaml"] + files + result = subprocess.run(args, cwd=tmp_path, capture_output=True) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + output_names = [f"group_{i}" for i in [1, 2]] + for n in output_names: + assert (tmp_path / "DataFiles" / f"{n}.mtz").is_file() +@@ -563,7 +602,10 @@ def test_reduce_with_grouping(dials_data, tmp_path, use_grouping): + """ + ssx = dials_data("cunir_serial_processed", pathlib=True) + ssx_data = dials_data("cunir_serial", pathlib=True) +- args = ["xia2.ssx_reduce", f"directory={ssx}"] ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, f"directory={ssx}"] + extra_args = [] + model = dials_data("cunir_serial", pathlib=True) / "2BW4.pdb" + extra_args.append(f"model={str(model)}") +@@ -596,7 +638,7 @@ grouping: + + result = subprocess.run(args + extra_args, cwd=tmp_path, capture_output=True) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + output_names = [f"group_{i}" if use_grouping else f"dose_{i}" for i in [1, 2]] + for n in output_names: + assert (tmp_path / "DataFiles" / f"{n}.mtz").is_file() +@@ -614,7 +656,10 @@ grouping: + + # now rerun with a res limit on one group. Should be able to just process straight from + # the group files for fast merging. +- args = ["xia2.ssx_reduce", "d_min=3.0", "steps=merge"] ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, "d_min=3.0", "steps=merge"] + if use_grouping: + args += list( + (tmp_path / "data_reduction" / "merge" / "group_1").glob("group*.expt") +@@ -632,7 +677,7 @@ grouping: + + result = subprocess.run(args, cwd=tmp_path, capture_output=True) + assert not result.returncode +- assert not result.stderr.decode() ++ assert not result.stderr + assert (tmp_path / "DataFiles" / "merged.mtz").is_file() + merged_mtz = mtz.object(file_name=os.fspath(tmp_path / "DataFiles" / "merged.mtz")) + assert abs(merged_mtz.n_reflections() - 416) < 10 # expect 298 from d_min=3.0 +@@ -671,7 +716,10 @@ def test_ssx_reduce_filter_options( + dials_data, tmp_path, cluster_args: List[str], expected_results: dict + ): + ssx = dials_data("cunir_serial_processed", pathlib=True) +- args = ["xia2.ssx_reduce", f"directory={ssx}"] + cluster_args ++ cmd = "xia2.ssx_reduce" ++ if os.name == "nt": ++ cmd += ".exe" ++ args = [cmd, f"directory={ssx}"] + cluster_args + + result = subprocess.run(args, cwd=tmp_path, capture_output=True) + assert not result.returncode and not result.stderr +@@ -699,8 +747,11 @@ def test_on_sacla_data(dials_data, tmp_path): + geometry = ( + sacla_path / "SACLA-MPCCD-run266702-0-subset-refined_experiments_level1.json" + ) ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + f"image={image}", + f"reference_geometry={geometry}", + "space_group = P43212", +@@ -732,8 +783,11 @@ def test_on_sacla_data_slice(dials_data, tmp_path): + fp = tmp_path / "sf.phil" + with open(fp, "w") as f: + f.write(find_spots_phil) ++ cmd = "xia2.ssx" ++ if os.name == "nt": ++ cmd += ".exe" + args = [ +- "xia2.ssx", ++ cmd, + f"image={image}", + f"reference_geometry={geometry}", + "space_group = P43212", +diff --git a/tests/regression/test_vmxi_thaumatin.py b/tests/regression/test_vmxi_thaumatin.py +index 8b5f59de..02f9f627 100644 +--- xia2/tests/regression/test_vmxi_thaumatin.py ++++ xia2/tests/regression/test_vmxi_thaumatin.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import os + import subprocess + + import pytest +@@ -12,8 +13,11 @@ def test_xia2(pipeline, regression_test, dials_data, tmp_path, ccp4): + master_h5 = ( + dials_data("vmxi_thaumatin", pathlib=True) / "image_15799_master.h5:1:20" + ) ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" + command_line = [ +- "xia2", ++ cmd, + f"pipeline={pipeline}", + "nproc=1", + "trust_beam_centre=True", +diff --git a/tests/test_run_xia2.py b/tests/test_run_xia2.py +index 1a71c2ce..af9b5e9f 100644 +--- xia2/tests/test_run_xia2.py ++++ xia2/tests/test_run_xia2.py +@@ -3,9 +3,13 @@ + + from __future__ import annotations + ++import os + import subprocess + + + def test_start_xia2(): +- result = subprocess.run(["xia2"]) ++ cmd = "xia2" ++ if os.name == "nt": ++ cmd += ".exe" ++ result = subprocess.run([cmd]) + assert result.returncode == 0