diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 24b12ee57447094bec3d20ea77940cf6c7091e56..6a5606c681ea6fd39476929d9034592d65fdd4d8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,25 @@ check-clang-format: - if: $CI_COMMIT_BRANCH allow_failure: true +check-python-quality: + image: corsika/devel:u-22.04 + stage: quality + tags: + - corsika + script: + - cd ${CI_PROJECT_DIR}/python # change into the Python directory + - pip3 install --user -e '.[test]' # install the package + test deps + - python3 --version + - python3 -m black -t py37 --check corsika tests + - python3 -m flake8 -v corsika tests + - python3 -m isort --atomic --check-only corsika tests + - python3 -m mypy corsika + rules: + - if: $CI_MERGE_REQUEST_ID + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_BRANCH + allow_failure: true + ### CodeQuality tool #### #include: # - template: Code-Quality.gitlab-ci.yml @@ -76,7 +95,8 @@ check-clang-format: # when: never # - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' # Run code quality job in merge request pipelines # - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' # Run code quality job in pipelines on the master branch (but not in other branch pipelines) -# - if: '$CI_COMMIT_TAG' # Run code quality job in pipelines for tags +# - if: '$CI_COMMIT_TAG' + ####### CONFIG ############## @@ -163,13 +183,11 @@ build_test-clang-14: artifacts: false - ####### BUILD-TEST-EXAMPLE (only non-Draft/non-WIP) ############## ########################################################## # generic example template job # normal pipeline for each commit -# artefacts are needed as input for python jobs .build_test_example: stage: build_test_example tags: @@ -204,8 +222,10 @@ build_test-clang-14: junit: - ${CI_PROJECT_DIR}/build/test_outputs/junit*.xml paths: - - ${CI_PROJECT_DIR}/build/build_examples/example_outputs + - ${CI_PROJECT_DIR}/build/build_examples/example_outputs #python tests need this - ${CI_PROJECT_DIR}/build/test_outputs/junit*.xml + - ${CI_PROJECT_DIR}/build/CMakeCache.txt #python tests need this + - ${CI_PROJECT_DIR}/build/bin #python tests need this # build_test_example for gcc build_test_example-u-22_04: @@ -225,9 +245,6 @@ build_test_example-clang-14: artifacts: false - - - ####### OPTIONAL ############## ########################################################## @@ -288,7 +305,7 @@ release-full-clang-14: artifacts: false - +###### COVERAGE ########## ########################################################## # the coverage generation should either run when manually requested, OR always on the master @@ -330,7 +347,7 @@ coverage: - +###### SANITY ########## ########################################################## sanity: @@ -378,19 +395,17 @@ sanity: - python3 -m black -t py37 corsika tests - python3 -m flake8 corsika tests - python3 -m pytest --cov=corsika tests - - cd ${CI_PROJECT_DIR} # reset the directory coverage: '/^TOTAL\s*\d+\s*\d+\s*(.*\%)/' -# the default Python version Ubuntu 22.04 is Python3.8 -python-3.8: +python-tests: extends: .python - image: corsika/analysis:python-3.9.5 + image: corsika/devel:u-22.04 needs: - job: build_test_example-u-22_04 artifacts: true artifacts: when: always - expire_in: 3 month + expire_in: 1 month paths: - - ${CI_PROJECT_DIR}/Python/python-test.log - allow_failure: true + - ${CI_PROJECT_DIR}/python/python-test.log + allow_failure: false diff --git a/modules/data b/modules/data index 331c0aad7c6c17fe456c4a80f8b2575ef341ce90..aa320e6a1203436766a374878e83e3685eae78e4 160000 --- a/modules/data +++ b/modules/data @@ -1 +1 @@ -Subproject commit 331c0aad7c6c17fe456c4a80f8b2575ef341ce90 +Subproject commit aa320e6a1203436766a374878e83e3685eae78e4 diff --git a/python/corsika/io/__init__.py b/python/corsika/io/__init__.py index 296572d6dee46a4b95a61b8ef18c549f120a1205..7c8d197d724ed4a45e6f51b2a6ae31e8c064ee27 100644 --- a/python/corsika/io/__init__.py +++ b/python/corsika/io/__init__.py @@ -8,8 +8,7 @@ the license. """ -from .hist import read_hist from .library import Library # all exported objects -__all__ = ["read_hist", "Library"] +__all__ = ["Library"] diff --git a/python/corsika/io/converters/__init__.py b/python/corsika/io/converters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..034aa1d4643e1cf19fe728e21790c4862b0626e3 --- /dev/null +++ b/python/corsika/io/converters/__init__.py @@ -0,0 +1,9 @@ +""" + The 'io' module provides for reading CORSIKA8 output files. + + (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu + + This software is distributed under the terms of the GNU General Public + Licence version 3 (GPL Version 3). See file LICENSE for a full version of + the license. +""" diff --git a/python/corsika/io/converters/arrow_to_numpy.py b/python/corsika/io/converters/arrow_to_numpy.py new file mode 100644 index 0000000000000000000000000000000000000000..dddb34b8a740633755d38e055dd2c5e507a8df3d --- /dev/null +++ b/python/corsika/io/converters/arrow_to_numpy.py @@ -0,0 +1,59 @@ +""" + Converter for the pyarrow data types to numpy ones + + (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + + This software is distributed under the terms of the GNU General Public + Licence version 3 (GPL Version 3). See file LICENSE for a full version of + the license. +""" + +import numpy as np +import pyarrow + + +def convert_to_numpy(pyarrow_table: pyarrow.lib.Table) -> np.ndarray: + """ + Converts a pyarrow Table to a numpy structured array + + Parameters + ---------- + pyarrow_table: pyarrow.lib.Table + PyArrow table of any dimension to be sliced + + Returns + ------- + np.ndarray: + converted table with the same column labels and data types + + """ + + # Type conversions for pyarrow data types to numpy ones + # https://arrow.apache.org/docs/python/data.html + # https://numpy.org/doc/stable/reference/arrays.dtypes.html#arrays-dtypes-constructing + type_conversions = { + pyarrow.int8(): "int8", + pyarrow.int16(): "int16", + pyarrow.int32(): "int32", + pyarrow.int64(): "int64", + pyarrow.uint8(): "uint8", + pyarrow.uint16(): "uint16", + pyarrow.uint32(): "uint32", + pyarrow.uint64(): "uint64", + pyarrow.float16(): "float16", + pyarrow.float32(): "float32", + pyarrow.float64(): "float64", + } + + # Perform type conversion of all fields + column_types = [ + type_conversions[pyarrow_table[key].type] for key in pyarrow_table.column_names + ] + dtypes = [(x, y) for (x, y) in zip(pyarrow_table.column_names, column_types)] + + # Make an empty array and then fill the values + np_table = np.zeros(pyarrow_table.num_rows, dtype=dtypes) + for key in pyarrow_table.column_names: + np_table[key] = pyarrow_table[key] + + return np_table diff --git a/python/corsika/io/hist.py b/python/corsika/io/hist.py deleted file mode 100644 index afa1418418f636eac2f00d77c58d58b5808c4875..0000000000000000000000000000000000000000 --- a/python/corsika/io/hist.py +++ /dev/null @@ -1,76 +0,0 @@ -""" - This file supports reading boost_histograms from CORSIKA8. - - (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu - - This software is distributed under the terms of the GNU General Public - Licence version 3 (GPL Version 3). See file LICENSE for a full version of - the license. -""" - -import boost_histogram as bh -import numpy as np - - -def read_hist(filename: str) -> bh.Histogram: - """ - Read a histogram produced with CORSIKA8's `SaveBoostHistogram()` function. - - Parameters - ---------- - filename: str - The filename of the .npy file containing the histogram. - - Returns - ------- - hist: bh.Histogram - An initialized bh.Histogram instance. - - Throws - ------ - ValueError: - If the histogram type is not supported. - """ - - # load the filenames - d = np.load(filename) - - # extract the axis and overflows information - axistypes = d["axistypes"].view("c") - overflow = d["overflow"] - underflow = d["underflow"] - - # this is where we store the axes that we extract from the file. - axes = [] - - # we now loop over the axes - for i, (at, has_overflow, has_underflow) in enumerate( - zip(axistypes, overflow, underflow) - ): - - # continuous axis - if at == b"c": - axes.append( - bh.axis.Variable( - d[f"binedges_{i}"], overflow=has_overflow, underflow=has_underflow - ) - ) - # discrete axis - elif at == b"d": - axes.append(bh.axis.IntCategory(d[f"bins_{i}"], growth=(not has_overflow))) - - # and unknown histogram type - else: - raise ValueError(f"'{at}' is not a valid C8 histogram axistype.") - - # create the histogram and fill it in - data = d["data"] - if data.dtype.names: # structured array -> mean and variance - h = bh.Histogram(*axes, storage=bh.storage.Weight()) - h.view(flow=True)[:]['value'] = data['mean'] - h.view(flow=True)[:]['variance'] = data['variance'] - else: - h = bh.Histogram(*axes) - h.view(flow=True)[:] = data - - return h diff --git a/python/corsika/io/library.py b/python/corsika/io/library.py index 1c2ff38e025e9a728bde4b9c7a5e0cad5982deb1..486181e5a7422e811721370955383385e8165526 100644 --- a/python/corsika/io/library.py +++ b/python/corsika/io/library.py @@ -58,13 +58,6 @@ class Library(object): """ return list(self.__outputs.keys()) - @property - def modules(self) -> Dict[str, str]: - """ - Return the list of registered outputs. - """ - pass - def get(self, name: str) -> Optional[outputs.Output]: """ Return the output with a given name. @@ -73,7 +66,7 @@ class Library(object): return self.__outputs[name] else: msg = f"Output with name '{name}' not available in this library." - logging.getLogger("corsika").warn(msg) + logging.getLogger("corsika").warning(msg) return None @staticmethod @@ -181,7 +174,6 @@ class Library(object): # loop over the subdirectories for subdir in dirs: - # read the config file for this output config = Library.load_config(op.join(path, subdir)) @@ -197,13 +189,12 @@ class Library(object): f"'{subdir}' does not contain a valid config." "Missing 'type' or 'name' keyword." ) - logging.getLogger("corsika").warn(msg) + logging.getLogger("corsika").warning(msg) continue # skip to the next output, don't error # we now have a valid component type, get the corresponding # type from the proccesses subdirectory try: - # instantiate the output and store it in our dict component = getattr(outputs, out_type)(op.join(path, subdir)) @@ -213,7 +204,7 @@ class Library(object): f"'{name}' encountered an error while reading. " "This process will be not be loaded." ) - logging.getLogger("corsika").warn(msg) + logging.getLogger("corsika").warning(msg) else: components[name] = component @@ -222,8 +213,8 @@ class Library(object): f"Unable to instantiate an instance of '{out_type}' " f"for a process called '{name}'" ) - logging.getLogger("corsika").warn(msg) - logging.getLogger("corsika").warn(e) + logging.getLogger("corsika").warning(msg) + logging.getLogger("corsika").warning(e) continue # skip to the next output, don't error # and we are done building - return the constructed outputs diff --git a/python/corsika/io/outputs/__init__.py b/python/corsika/io/outputs/__init__.py index 81ecd786e25aef93a2b84cea6e6419b133078518..54731dd42d775073d5d15e3d21e914706b5a7755 100644 --- a/python/corsika/io/outputs/__init__.py +++ b/python/corsika/io/outputs/__init__.py @@ -7,16 +7,15 @@ the license. """ -from .observation_plane import ObservationPlane -from .track_writer import TrackWriter -from .longitudinal_profile import LongitudinalProfile from .bethe_bloch import BetheBlochPDG -from .particle_cut import ParticleCut from .energy_loss import EnergyLoss +from .longitudinal_profile import LongitudinalProfile +from .observation_plane import ObservationPlane from .output import Output +from .particle_cut import ParticleCut +from .primary import Particle, PrimaryParticle from .radio_process import RadioProcess from .track_writer import TrackWriter -from .primary import PrimaryParticle, Particle __all__ = [ "Output", @@ -25,7 +24,8 @@ __all__ = [ "LongitudinalProfile", "BetheBlochPDG", "ParticleCut", - "EnergyLoss" "RadioProcess", + "EnergyLoss", + "RadioProcess", "PrimaryParticle", "Particle", ] diff --git a/python/corsika/io/outputs/bethe_bloch.py b/python/corsika/io/outputs/bethe_bloch.py index b12d29036de5553d139c9cee0f7c8120f4fd4f7b..42e65943c4a2ac5e82752534a996e2d018edf089 100644 --- a/python/corsika/io/outputs/bethe_bloch.py +++ b/python/corsika/io/outputs/bethe_bloch.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class BetheBlochPDG(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for BetheBlochPDG. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) diff --git a/python/corsika/io/outputs/energy_loss.py b/python/corsika/io/outputs/energy_loss.py index 76846dd2c83d497f68aaf38d3f3e4823b91ee8a3..82b98b0fe2d02530b10231890341359161d4c418 100644 --- a/python/corsika/io/outputs/energy_loss.py +++ b/python/corsika/io/outputs/energy_loss.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class EnergyLoss(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for EnergyLoss. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) @@ -84,4 +87,4 @@ class EnergyLoss(Output): """ Return a string representation of this class. """ - return f"EnergyLess('{self.config['name']}')" + return f"EnergyLoss('{self.config['name']}')" diff --git a/python/corsika/io/outputs/longitudinal_profile.py b/python/corsika/io/outputs/longitudinal_profile.py index 00000567d827a12e94c198a0ef1102b1c010a74e..f4126c05a30d2460bdb3c1e1534a01b57a38d8fb 100644 --- a/python/corsika/io/outputs/longitudinal_profile.py +++ b/python/corsika/io/outputs/longitudinal_profile.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class LongitudinalProfile(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for LongitudinalProfile. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) diff --git a/python/corsika/io/outputs/observation_plane.py b/python/corsika/io/outputs/observation_plane.py index 271d145b4f076f1c7d747d6cc5e05b4a183d3655..d98c31646d73dad382e23e17070a0aa4b27df0c2 100644 --- a/python/corsika/io/outputs/observation_plane.py +++ b/python/corsika/io/outputs/observation_plane.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class ObservationPlane(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for ObservationPlane. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) diff --git a/python/corsika/io/outputs/particle_cut.py b/python/corsika/io/outputs/particle_cut.py index a55b5fe718cf788fdba051dfa891e77b2d75ede7..d797923374febd1098126dc04f9d9541e886285d 100644 --- a/python/corsika/io/outputs/particle_cut.py +++ b/python/corsika/io/outputs/particle_cut.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class ParticleCut(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for ParticleCut. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) diff --git a/python/corsika/io/outputs/radio_process.py b/python/corsika/io/outputs/radio_process.py index 62ceb3f1905ccaf938b1a6f8fba799e655eb7750..152b6d6347a5803481bb519f016c9f10deeebc1c 100644 --- a/python/corsika/io/outputs/radio_process.py +++ b/python/corsika/io/outputs/radio_process.py @@ -10,8 +10,9 @@ import logging import os.path as op from typing import Any -import pyarrow.parquet as pq + import pandas as pd +import pyarrow.parquet as pq from .output import Output @@ -47,8 +48,7 @@ class RadioProcess(Output): f"An error occured loading a RadioProcess: {e}" ) - - def load_data(self, path: str): + def load_data(self, path: str) -> dict: """ Load the data associated with this radio process. @@ -59,7 +59,7 @@ class RadioProcess(Output): """ data = pq.read_table(op.join(path, "antennas.parquet")) - nshowers = data.to_pandas()['shower'].iloc[-1] + 1 + nshowers = data.to_pandas()["shower"].iloc[-1] + 1 antennas = list(self.config["antennas"].keys()) # check that we got some events @@ -78,13 +78,17 @@ class RadioProcess(Output): # loop over each of the antennas for name in antennas: sampling_period = self.config["antennas"][name]["number of bins"] - antenna_data = data[ant_nr*sampling_period:(ant_nr+1)*sampling_period].to_pandas() + start = ant_nr * sampling_period + stop = (ant_nr + 1) * sampling_period + antenna_data = data[start:stop].to_pandas() times = antenna_data["Time"] Ex = antenna_data["Ex"] Ey = antenna_data["Ey"] Ez = antenna_data["Ez"] - dictionary[name] = pd.DataFrame({'time': times, 'Ex': Ex, 'Ey': Ey, 'Ez': Ez}) - ant_nr = ant_nr+ 1 + dictionary[name] = pd.DataFrame( + {"time": times, "Ex": Ex, "Ey": Ey, "Ez": Ez} + ) + ant_nr = ant_nr + 1 dataset[str(i)] = dictionary @@ -126,14 +130,14 @@ class RadioProcess(Output): ) ) - def get_antenna_list(self): + def get_antenna_list(self) -> list: """ Return a list with the names of the antennas of this RadioProcess. """ antennas = list(self.config["antennas"].keys()) return antennas - def get_antennas(self): + def get_antennas(self) -> dict: """ Return a pandas dataframe with all the information for the antennas of this RadioProcess. This information is shower number, antenna @@ -150,13 +154,22 @@ class RadioProcess(Output): nr_bins = self.config["antennas"][name]["number of bins"] sampling_frequency = self.config["antennas"][name]["sampling frequency"] location = self.config["antennas"][name]["location"] - dictionary[name] = pd.DataFrame({'type': type, 'start time': start_time, 'duration': duration, - 'number of bins': nr_bins, 'sampling frequency': sampling_frequency, - 'x': [location[0]], 'y': [location[1]], 'z': [location[2]]}) + dictionary[name] = pd.DataFrame( + { + "type": type, + "start time": start_time, + "duration": duration, + "number of bins": nr_bins, + "sampling frequency": sampling_frequency, + "x": [location[0]], + "y": [location[1]], + "z": [location[2]], + } + ) return dictionary - def get_units(self): + def get_units(self) -> dict: """ Return the units of the antennas of this RadioProcess. """ diff --git a/python/corsika/io/outputs/track_writer.py b/python/corsika/io/outputs/track_writer.py index 0859660d590182578e08ad4beaef5bb9a5c53498..7b4fd4f414d56f6716817ddb78c688806f5d9f56 100644 --- a/python/corsika/io/outputs/track_writer.py +++ b/python/corsika/io/outputs/track_writer.py @@ -13,6 +13,7 @@ from typing import Any import pyarrow.parquet as pq +from ..converters import arrow_to_numpy from .output import Output @@ -72,11 +73,13 @@ class TrackWriter(Output): return self.__data elif dtype == "pandas": return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) else: raise ValueError( ( f"Unknown format '{dtype}' for TrackWriter. " - "We currently only support ['arrow', 'pandas']." + "We currently only support ['arrow', 'pandas', 'numpy']." ) ) diff --git a/python/setup.cfg b/python/setup.cfg index 2c8d668ffdf9a3a8135dc52fc6e4c79b66c06e72..e0a574a473491369997c050a47a5fc51c9a9e570 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -67,3 +67,7 @@ ignore_missing_imports = True # ignore missing types for pyarow [mypy-pyarrow.*] ignore_missing_imports = True + +# ignore missing types for pandas +[mypy-pandas.*] +ignore_missing_imports = True diff --git a/python/setup.py b/python/setup.py index c1460c9d6cb5f759b50abd609e31061ef0d1751b..20da6eaa249c572540bd66ba574a4995705d5578 100644 --- a/python/setup.py +++ b/python/setup.py @@ -32,7 +32,7 @@ setup( keywords=["cosmic ray", "physics", "air shower", "simulation"], packages=find_packages(), python_requires=">=3.6, <4", - install_requires=["numpy", "pyyaml", "pyarrow", "boost_histogram", "xarray"], + install_requires=["numpy", "pyyaml", "pyarrow", "pandas"], extras_require={ "test": [ "pytest", @@ -42,8 +42,9 @@ setup( "coverage", "pytest-cov", "flake8", + "types-PyYAML", + "pandas-stubs", ], - "pandas": ["pandas"], }, scripts=[], project_urls={"code": "https://gitlab.iap.kit.edu/AirShowerPhysics/corsika"}, diff --git a/python/tests/__init__.py b/python/tests/__init__.py index c7d9b672b4c1bde7cc7725c619c0390a79e682c7..4e71ff9c93bb28bec001dad43656772ca9709fef 100644 --- a/python/tests/__init__.py +++ b/python/tests/__init__.py @@ -29,25 +29,34 @@ def find_build_directory() -> str: If the build directory cannot be found or it is empty. """ - # check if we are running on Gitlab - gitlab_build_dir = os.getenv("CI_BUILDS_DIR") - # if we are running on Gitlab - if gitlab_build_dir is not None: - build_dir = op.abspath(gitlab_build_dir) + is_on_CI = os.getenv("CI_PROJECT_DIR") is not None + if is_on_CI: + build_dir = op.abspath(op.join(os.getenv("CI_PROJECT_DIR"), "build")) + else: # otherwise, we are running locally - build_dir = op.abspath( - op.join( - op.dirname(__file__), - op.pardir, - op.pardir, - "build", + here = op.dirname(os.path.realpath(__file__)) + for name in ["build", "corsika-build"]: + build_dir = op.realpath( + op.join( + here, + op.pardir, + op.pardir, + op.pardir, + "corsika-build", + ) ) - ) + if op.isdir(build_dir): + break # check that the build directory contains 'CMakeCache.txt' if not op.exists(op.join(build_dir, "CMakeCache.txt")): - raise RuntimeError("Python tests cannot find C8 build directory.") + msg = "Python tests cannot find C8 build dir.\n" + msg += f"Is on CI: {is_on_CI}\n" + msg += f"Path for CI: {os.getenv('CI_PROJECT_DIR')}\n" + msg += f"Checked in {build_dir}\n" + msg += f"Contents: {os.listdir(build_dir)}" + raise RuntimeError(msg) return build_dir diff --git a/python/tests/io/test_hist.py b/python/tests/io/test_hist.py deleted file mode 100644 index 3bd11003ca663337f322ed9fea4a7f7559e75d03..0000000000000000000000000000000000000000 --- a/python/tests/io/test_hist.py +++ /dev/null @@ -1,70 +0,0 @@ -""" - Tests for `corsika.io.hist` - - (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu - - This software is distributed under the terms of the GNU General Public - Licence version 3 (GPL Version 3). See file LICENSE for a full version of - the license. -""" -import os -import os.path as op -import subprocess - -import corsika - -from .. import build_directory - -# the directory containing 'testSaveBoostHistogram' -bindir = op.join(build_directory, "bin") - - -def generate_hist() -> str: - """ - Generate a test with `testSaveBoostHistogram`. - - Returns - ------- - str - The path to the generated histogram. - bool - If True, this file was regenerated. - """ - - # we construct the name of the bin - bin = op.join(bindir, "testFramework") - - # check if a histogram already exists - if op.exists(op.join(bin, "hist.npz")): - return op.join(bin, "hist.npz"), False - else: - # run the program - this generates "hist.npz" in the CWD - subprocess.call([bin, "saveHistogram"]) - - return op.join(os.getcwd(), "hist.npz"), True - - -def test_corsika_io() -> None: - """ - Test I can corsika.io without a further import. - """ - corsika.io.read_hist - - -def test_corsika_read_hist() -> None: - """ - Check that I can read in the test histograms with `read_hist`. - """ - - # generate a test histogram - filename, delete = generate_hist() - - # and try and read in a histogram - h = corsika.io.read_hist(filename) - - # and delete the generated histogram - if delete: - os.remove(filename) - - # and check that it isn't empty - assert not h.empty() diff --git a/python/tests/io/test_library.py b/python/tests/io/test_library.py new file mode 100644 index 0000000000000000000000000000000000000000..d029c59e33f889724a47806a40b7cfdd7fc50339 --- /dev/null +++ b/python/tests/io/test_library.py @@ -0,0 +1,72 @@ +""" + Tests for `corsika.io.outputs.energy_loss` + + (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu + + This software is distributed under the terms of the GNU General Public + Licence version 3 (GPL Version 3). See file LICENSE for a full version of + the license. +""" +import os +import os.path as op +import subprocess + +import pytest + +from corsika.io import Library + +from .. import build_directory + +bindir = op.join(build_directory, "bin") + + +def generate_data() -> str: + """ + Generate a test with `testOutput`. + + Returns + ------- + str + The path to the generated data. + """ + + # Expected output directory that is made from the testOutput binary + output_dir = op.join(os.getcwd(), "out_test/check") + + if not op.exists(output_dir): # only make if not already run + binary = op.join(bindir, "testOutput") + + # ensure that the binary exists (not trivial on the CI) + if not op.exists(binary): + msg = f"Could not find testOutput binary at {binary}\n" + msg += f"Binary dir contains {os.listdir(bindir)}" + raise RuntimeError(msg) + + subprocess.call([binary, "OutputManager"]) + + # Check if it still doesn't exist + if not op.exists(output_dir): + msg = "After running binary, could not find expected" + msg += f" output dir {output_dir}\n" + msg += "The binary did not execute successfully or the" + msg += " OutputManager tests have changed" + raise RuntimeError(msg) + + return output_dir + + +def test_basic_Library() -> None: + dir_to_test = generate_data() + lib = Library(dir_to_test) + + assert 0 == len(lib.names) + assert len(lib.summary) + assert len(lib.config) + + # Check what happens for an unknown output subdir + assert lib.get("Does not exits") is None + + +def test_bad_Library() -> None: + with pytest.raises(ValueError): + Library("This does not exist")