diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..f804b1ba --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,33 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: RAMP tests + +on: + push: + branches: [ "main", "development" ] + pull_request: + branches: [ "main", "development" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r tests/requirements.txt + - name: Test with pytest + run: | + pytest tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec54f31..f5f773fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Release History 0.4.2 (dev) ------------------ +**|new|** continuous integration setup + +**|new|** first automated tests + **|fixed|** installation options have been debugged and the documentation updated accordingly **|fixed|** automated download of example applications via the `download_example` functions now includes previously missing .csv files diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..feb62d07 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest +scipy \ No newline at end of file diff --git a/tests/test_calc_peak_time_range.py b/tests/test_calc_peak_time_range.py new file mode 100644 index 00000000..61612aef --- /dev/null +++ b/tests/test_calc_peak_time_range.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jun 5 16:21:32 2023 + +@author: claudia +""" + +import numpy as np +import scipy.stats as stats +from ramp.core.initialise import user_defined_inputs +from ramp.core.stochastic_process import calc_peak_time_range + + +def test_peak_time_range_values(): + """ + Perform a statistical analysis on the peak time range values obtained from the calc_peak_time_range function. + + Inputs: + - user_list: A list containing all the user types. + - num_repetitions: The desired number of repetitions for the statistical analysis. + + Outputs: + - None (The function performs an assertion to check the normality of the mean values). + """ + + user_list = user_defined_inputs(j=1, fname=None) + + num_repetitions = 100 # Set the desired number of repetitions + results = [] + + for _ in range(num_repetitions): + peak_time_range = calc_peak_time_range(user_list, peak_enlarge=0.15) + results.append(peak_time_range) + + statistics_dict = {} + + # Performing statistical analysis on the results + for i, arr in enumerate(results): + statistics_dict[i] = {'mean': np.mean(arr)} + + # Extract the mean values + mean_sample = [inner_dict['mean'] for inner_dict in statistics_dict.values()] + # Perform the normality test (Shapiro-Wilk in this sample) + _, p_value = stats.shapiro(mean_sample) + assert p_value > 0.05 diff --git a/tests/test_input_file_conversion.py b/tests/test_input_file_conversion.py index 7e5ddba4..b327d9c7 100644 --- a/tests/test_input_file_conversion.py +++ b/tests/test_input_file_conversion.py @@ -4,7 +4,7 @@ from ramp.core.core import User, Appliance from ramp.core.initialise import initialise_inputs -e + from ramp.ramp_convert_old_input_files import convert_old_user_input_file @@ -19,15 +19,12 @@ class TestConversion: def setup_method(self): self.input_files_to_run = [1, 2, 3] self.file_suffix = "_test" - os.chdir( - "ramp" - ) # for legacy code to work the loading of the input file has to happen from the ramp folder self.py_fnames = [ - os.path.join("input_files", f"input_file_{i}.py") + os.path.join("ramp","example", f"input_file_{i}.py") for i in self.input_files_to_run ] self.xlsx_fnames = [ - os.path.join("test", f"input_file_{i}{self.file_suffix}.xlsx") + os.path.join("ramp","test", f"input_file_{i}{self.file_suffix}.xlsx") for i in self.input_files_to_run ] for fname in self.xlsx_fnames: @@ -56,7 +53,7 @@ def test_convert_py_to_xlsx(self): for i, j in enumerate(self.input_files_to_run): old_user_list = load_usecase(j=j) convert_old_user_input_file( - self.py_fnames[i], output_path="test", suffix=self.file_suffix + self.py_fnames[i], output_path=os.path.join("ramp","test"), suffix=self.file_suffix ) new_user_list = load_usecase(fname=self.xlsx_fnames[i]) for old_user, new_user in zip(old_user_list, new_user_list): @@ -130,7 +127,7 @@ def test_provide_no_appliance_window_when_declaring_one(): def test_A(): user = User("test user", 1) - old_params = dict(n=1, P=200, w=1, t=0) + old_params = dict(power=200, num_windows=1, func_time=0) win_start = 390 win_stop = 540 appliance1 = user.Appliance(user, **old_params) diff --git a/tests/test_rand_total_time_of_use.py b/tests/test_rand_total_time_of_use.py new file mode 100644 index 00000000..e4e3c99f --- /dev/null +++ b/tests/test_rand_total_time_of_use.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 28 15:40:12 2023 + +@author: claudia +""" + +from ramp.core.core import Appliance +from ramp.core.core import User + + +import pytest + + +@pytest.fixture +def appliance_instance(): + + # Create a User instance (you may need to provide the required arguments for User) + user = User(user_name="Test User", num_users=1) + appliance = Appliance( + user="Test User", + name="Test Appliance", + func_time=100, # Set an appropriate func_time + func_cycle=20, + time_fraction_random_variability=0.1 + ) + return appliance + + +# Define the test class for the Appliance class +class TestAppliance: + + #Test that the method returns an integer + @pytest.mark.usefixtures("appliance_instance") + def test_returns_integer_value(self, appliance_instance): + result = appliance_instance.rand_total_time_of_use([0, 480], [600, 1080], [1200, 1440]) + assert isinstance(result, int) + + #Tests that the method returns a value greater than func_cycle + @pytest.mark.usefixtures("appliance_instance") + def test_rand_time_equal_or_greater_than_func_cycle(self, appliance_instance): + # Define windows with total available time + rand_window_1 = [0, 100] + rand_window_2 = [200, 300] + rand_window_3 = [400, 500] + appliance_instance.func_cycle = 50 + # Generate a sample of 'rand_time' values + sample_size = 100 + rand_time_sample = [] + for _ in range(sample_size): + rand_time = appliance_instance.rand_total_time_of_use(rand_window_1, rand_window_2, rand_window_3) + rand_time_sample.append(rand_time) + assert all(rand_time >= appliance_instance.func_cycle for rand_time in rand_time_sample) + + + # Tests that the method returns a value less than or equal to 0.99 * total_time + @pytest.mark.usefixtures("appliance_instance") + def test_rand_time_less_than_99_percent_total_time(self, appliance_instance): + rand_window_1 = [0, 100] + rand_window_2 = [200, 300] + rand_window_3 = [400, 500] + appliance_instance.func_time = 200 + appliance_instance.time_fraction_random_variability = 0.5 + # Call the method from the class + rand_time = appliance_instance.rand_total_time_of_use(rand_window_1, rand_window_2, rand_window_3) + total_time = (rand_window_1[1] - rand_window_1[0]) + (rand_window_2[1] - rand_window_2[0]) + (rand_window_3[1] - rand_window_3[0]) + assert rand_time <= 0.99 * total_time + diff --git a/tests/test_switch_on.py b/tests/test_switch_on.py new file mode 100644 index 00000000..456bd35a --- /dev/null +++ b/tests/test_switch_on.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Sep 5 10:47:58 2023 + +@author: claudia +""" + +import pytest +from ramp.core.core import Appliance +from scipy import stats + + + +class TestRandSwitchOnWindow: + + # Test when self.fixed = yes, all the apps are switched on together (should return the total number of apps) + def test_all_appliances_switched_on_together(self): + appliance = Appliance(user=None, number=5, fixed='yes') + coincidence = appliance.calc_coincident_switch_on() + assert isinstance(coincidence, int) + assert coincidence == appliance.number + + # Test when self.fixed= no and the index value lies in peak_time_range (test whether the coincidence values are normally distributed or not) + def test_coincidence_normality_on_peak(self): + # Create an instance of the Appliance class with the desired parameters + appliance = Appliance(user=None, number=10, fixed='no') + + # Generate a sample of 'coincidence' values + sample_size = 30 + coincidence_sample = [] + for _ in range(sample_size): + coincidence = appliance.calc_coincident_switch_on(inside_peak_window=True) + coincidence_sample.append(coincidence) + + # Perform the Shapiro-Wilk test for normality + _, p_value = stats.shapiro(coincidence_sample) + + # Assert that the p-value is greater than a chosen significance level + assert p_value > 0.05, "The 'coincidence' values are not normally distributed." + + # Tests that the method returns a list of indexes within the available functioning windows when there are multiple available functioning windows and the random time is larger than the duration of the appliance's function cycle. + def test_happy_path(self): + appliance = Appliance(user=None, func_cycle=2) + appliance.free_spots = [slice(0, 5), slice(10, 15)] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert all(index in range(0, 5) or index in range(10, 15) for index in indexes) + + # Tests that the method returns None when there are no available functioning windows. + def test_edge_case_no_windows(self): + appliance = Appliance(user=None, func_cycle=2) + appliance.free_spots = [] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert indexes is None + + # Tests that the method returns None when the available functioning window is smaller than the duration of the appliance's function cycle. + def test_edge_case_small_window(self): + appliance = Appliance(user=None, func_cycle=5) + appliance.free_spots = [slice(0, 3)] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert indexes is None + + # Tests that the method returns a list of indexes within the available functioning window when the available functioning window is equal to the duration of the appliance's function cycle. + def test_edge_case_equal_window(self): + appliance = Appliance(user=None, func_cycle=5) + appliance.free_spots = [slice(0, 5)] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert all(index in range(0, 5) for index in indexes) + + # Tests that the method returns a list of indexes within the available functioning window when the available functioning window is larger than the duration of the appliance's function cycle. + def test_edge_case_large_window(self): + appliance = Appliance(user=None, func_cycle=2) + appliance.free_spots = [slice(0, 10)] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert all(index in range(0, 10) for index in indexes) + + # Tests that the method returns a list of indexes within the available functioning windows when the appliance has multiple free spots. + def test_edge_case_multiple_free_spots(self): + appliance = Appliance(user=None, func_cycle=2) + appliance.free_spots = [slice(0, 5), slice(10, 15), slice(20, 25)] + indexes = appliance.rand_switch_on_window(rand_time=6) + assert all(index in range(0, 5) or index in range(10, 15) or index in range(20, 25) for index in indexes) + + \ No newline at end of file