diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8269832 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: Run Unit and Function Tests + +on: + push: + branches: + - '*' + pull_request: + branches: [ master ] + +jobs: + run_styleCheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: mamba-org/setup-micromamba@v3 + with: + environment-file: environment.yml + init-shell: bash + - run: pycodestyle $(git ls-files "*.py") + shell: bash -l {0} + run_unit_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: mamba-org/setup-micromamba@v3 + with: + environment-file: environment.yml + init-shell: bash + - run: | + python -m unittest tests/test_my_utils.py + shell: bash -l {0} + run_function_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: mamba-org/setup-micromamba@v3 + with: + environment-file: environment.yml + init-shell: bash + - run: | + bash tests/test_print_fires.sh + shell: bash -l {0} + diff --git a/README.md b/README.md index c89ccdb..56e8d95 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This file contains utilities and functions to enable the easy reading and searching inside of csv files. While tested and confirmed to run with the Agrofood_co2_emission file, it can easily be applied to csv files of any shape or size. It currently does not ship with any relevant data. Data for analysis should be moved into the same folder as the python scripts. -UPDATE: The newest version of this package includes new functionality to perform basic rudimentary statistical analysis on output lists. See "Usage" for more information +The newest version of this package includes new functionality to perform basic rudimentary statistical analysis on output lists. See "Usage" for more information ## Installation @@ -24,7 +24,7 @@ python print_fires.py -file "path/to/my/file" -q_col int_column_to_search \ -q_val "str_value_to_search" -res_col int_column_to_find(optional) ''' -UPDATE: The newest version of this package includes new functionality to perform basic rudimentary statistical analysis on output lists. To ignore, simply use the package as previously designed. To get the mean, median, or standard deviation of the output list, use the '-stat' flag: +The newest version of this package includes new functionality to perform basic rudimentary statistical analysis on output lists. To ignore, simply use the package as previously designed. To get the mean, median, or standard deviation of the output list, use the '-stat' flag: '''bash python print_fires.py -file "path/to/my/file" -q_col int_column_to_search \ @@ -52,6 +52,10 @@ python3 print_fires.py -file "environment.yml" -q_col 0 \ ''' returns "Expected a .csv file, got .yml" +## Contributing + +New to version 4.0, automatic test architecture is now implemented. Ensure that tests are written appropriately in the "tests" folder, and add the appropriate text in the test.yml file. This ensures the tests will be automatically run upon pushing and pulling. + ## Summary of Changes: Updated get_col to correctly handle errors and return only integer output. diff --git a/environment.yml b/environment.yml index c47f8c6..fa22ffa 100644 --- a/environment.yml +++ b/environment.yml @@ -2,26 +2,7 @@ name: swe4s channels: - conda-forge dependencies: - - bzip2=1.0.8=h4e30115_10 - - ca-certificates=2026.7.22=hbd8a1cb_0 - - icu=78.3=py310h579977c_2 - - libcxx=23.1.0=h55c6f16_0 - - libexpat=2.8.1=hf6b4638_1 - - libffi=3.7.0=h47dc5ef_1 - - liblzma=5.8.3=h8088a28_1 - - libmpdec=4.0.0=h84a0fba_2 - - libpython=3.14.7=h4311e70_106_cp314 - - libsqlite=3.53.4=hca69786_1 - - libzlib=1.3.2=h8088a28_3 - - ncurses=6.6=he64c551_1 - - openssl=3.6.4=h55eecbc_0 - - pip=26.2.1=pyh145f28c_0 - - pycodestyle=2.14.0=pyhd8ed1ab_0 - - python=3.14.7=h0ae1c2c_106_cp314 - - python_abi=3.14=9_cp314 - - readline=8.3=h8b90a29_1 - - tk=8.6.13=hbeba79b_4 - - tzdata=2026c=h151e31d_0 - - zstd=1.5.7=hf451053_7 + - pycodestyle + - python prefix: "/Users/theurdragon/miniforge3/envs/swe4s" diff --git a/print_fires.py b/print_fires.py index b1937e7..e259191 100644 --- a/print_fires.py +++ b/print_fires.py @@ -16,7 +16,8 @@ query_column = args.q_col result_column = args.res_col file_name = args.file -fires = my_utils.get_column(file_name, query_column, query_value, result_column) +fires = my_utils.get_column(file_name, query_column, + query_value, result_column) if args.stat: if args.stat == "mean": diff --git a/tests/__pycache__/test_my_utils.cpython-314.pyc b/tests/__pycache__/test_my_utils.cpython-314.pyc new file mode 100644 index 0000000..2b23c84 Binary files /dev/null and b/tests/__pycache__/test_my_utils.cpython-314.pyc differ diff --git a/tests/test_file.csv b/tests/test_file.csv new file mode 100644 index 0000000..ae89fa3 --- /dev/null +++ b/tests/test_file.csv @@ -0,0 +1,11 @@ +Area,Year,Savanna fires,Forest fires,Crop Residues,Rice Cultivation,Drained organic soils (CO2),Pesticides Manufacturing,Food Transport,Forestland,Net Forest conversion,Food Household Consumption,Food Retail,On-farm Electricity Use,Food Packaging,Agrifood Systems Waste Disposal,Food Processing,Fertilizers Manufacturing,IPPU,Manure applied to Soils,Manure left on Pasture,Manure Management,Fires in organic soils,Fires in humid tropical forests,On-farm energy use,Rural population,Urban population,Total Population - Male,Total Population - Female,total_emission,Average Temperature °C +Afghanistan,1990,14.7237,0.0557,205.6077,686.0,0.0,11.807482956246297,63.1152,-2388.803,0.0,79.0851,109.6446,14.2666,67.63136607385836,691.7888,252.2141899664796,11.997,209.9778,260.1431,1590.5319,319.1763,0.0,0.0,,9655167.0,2593947.0,5348387.0,5346409.0,2198.9635389965842,0.5361666666666667 +Afghanistan,1991,14.7237,0.0557,209.4971,678.16,0.0,11.712073091310696,61.2125,-2388.803,0.0,80.4885,116.6789,11.4182,67.63136607385836,710.8212,252.2141899664796,12.8539,217.0388,268.6292,1657.2364,342.3079,0.0,0.0,,10230490.0,2763167.0,5372959.0,5372208.0,2323.8766291316488,0.02066666666666665 +Afghanistan,1992,14.7237,0.0557,196.5341,686.0,0.0,11.712073091310696,53.317,-2388.803,0.0,80.7692,126.1721,9.2752,67.63136607385836,743.6751,252.2141899664796,13.4929,222.1156,264.7898,1653.5068,349.1224,0.0,0.0,,10995568.0,2985663.0,6028494.0,6028939.0,2356.3042291316488,-0.25958333333333333 +Afghanistan,1993,14.7237,0.0557,230.8175,686.0,0.0,11.712073091310696,54.3617,-2388.803,0.0,85.0678,81.4607,9.0635,67.63136607385836,791.9246,252.2141899664796,14.0559,201.2057,261.7221,1642.9623,352.2947,0.0,0.0,,11858090.0,3237009.0,7003641.0,7000119.0,2368.470529131649,0.10191666666666666 +Afghanistan,1994,14.7237,0.0557,242.0494,705.6,0.0,11.712073091310696,53.9874,-2388.803,0.0,88.8058,90.4008,8.3962,67.63136607385836,831.9181,252.2141899664796,15.1269,182.2905,267.6219,1689.3593,367.6784,0.0,0.0,,12690115.0,3482604.0,7733458.0,7722096.0,2500.768729131649,0.37224999999999997 +American Samoa,1990,0.0,0.0,,926.3422784724489,0.0,11.481084565896976,16.9802,-58.8991,7.9612,0.5104,8.4872,17.00466278502689,77.39365076150476,1.6937,94.6238,2005.7921290780253,,,2148.251692850715,,0.0,0.0,41.4996,8962.0,38076.0,24538.0,23281.0,5299.122498513618,0.5665 +American Samoa,1991,0.0,0.0,,926.3422784724489,0.0,11.481084565896976,16.7829,-58.8991,7.9612,0.5104,8.3916,17.00466278502689,77.39365076150476,1.7413,92.7057,2005.7921290780253,,,2148.251692850715,,0.0,0.0,40.8094,8763.0,39612.0,25117.0,23874.0,5296.268898513617,0.599 +American Samoa,1992,0.0,0.0,,926.3422784724489,0.0,11.481084565896976,17.339,-58.8991,7.9612,0.5104,8.7751,17.00466278502689,77.39365076150476,1.7848,97.5009,2005.7921290780253,,,2148.251692850715,,0.0,0.0,42.9095,8535.0,41058.0,25656.0,24422.0,5304.147298513618,0.3661666666666667 +American Samoa,1993,0.0,0.0,,926.3422784724489,0.0,11.481084565896976,16.9225,-58.8991,7.9612,0.5104,8.4394,17.00466278502689,77.39365076150476,1.8251,94.3041,2005.7921290780253,,,2148.251692850715,,0.0,0.0,41.4996,8291.0,42429.0,26198.0,24976.0,5298.828698513617,-0.03266666666666668 +test_val,STRING,a,a,,926.3422784724489,0.0,11.481084565896976,16.9225,-58.8991,7.9612,0.5104,8.4394,17.00466278502689,77.39365076150476,1.8251,94.3041,2005.7921290780253,,,2148.251692850715,,0.0,0.0,41.4996,8291.0,42429.0,26198.0,24976.0,5298.828698513617,-0.03266666666666668 \ No newline at end of file diff --git a/tests/test_my_utils.py b/tests/test_my_utils.py new file mode 100644 index 0000000..f3e4d91 --- /dev/null +++ b/tests/test_my_utils.py @@ -0,0 +1,184 @@ +import unittest +from random import uniform +from random import gauss +import os +import sys + +# Allows running from main directory, my_utils is there. +module_path = os.path.abspath(".") + +sys.path.append(module_path) + +sys.path.append("orig/unit_testing") + +import my_utils # noqa + + +class TestUtils(unittest.TestCase): + + """get_mean() Tests""" + # Test mean function even + def test_mean_pos4(self): + lst = [1, 2, 4, 5] + m = my_utils.get_mean(lst) + self.assertEqual(m, 3) + + def test_mean_neg4(self): + lst = [-1, -2, -4, -5] + m = my_utils.get_mean(lst) + self.assertEqual(m, -3) + + # Test mean function odd + def test_mean_pos5(self): + lst = [2, 3, 4, 5, 6] + m = my_utils.get_mean(lst) + self.assertEqual(m, 4) + + def test_mean_neg5(self): + lst = [-2, -3, -4, -5, -6] + m = my_utils.get_mean(lst) + self.assertEqual(m, -4) + + # Test mean function float + def test_mean_posFloat(self): + lst = [1.5, 2.5, 4.5, 5.5] + m = my_utils.get_mean(lst) + self.assertEqual(m, 3.5) + + def test_mean_negFloat(self): + lst = [-1.5, -2.5, -4.5, -5.5] + m = my_utils.get_mean(lst) + self.assertEqual(m, -3.5) + + # Assorted mean error handling + def test_mean_str(self): + lst = ["a", 1, 2] + self.assertRaises(SystemExit, my_utils.get_mean, lst) + + def test_mean_empty(self): + lst = [] + self.assertRaises(SystemExit, my_utils.get_mean, lst) + + def test_mean_none(self): + lst = None + self.assertRaises(SystemExit, my_utils.get_mean, lst) + + # Test mean large numbers + def test_mean_rand(self): + lst = [uniform(-10, 10) for _ in range(100000)] + m = my_utils.get_mean(lst) + self.assertAlmostEqual(m, 0, 1) + + """get_median() Tests""" + # Test median function even + def test_median_pos4(self): + lst = [1, 2, 4, 10] + m = my_utils.get_median(lst) + self.assertEqual(m, 3) + + def test_median_neg4(self): + lst = [-1, -2, -4, -10] + m = my_utils.get_median(lst) + self.assertEqual(m, -3) + + # Test median function odd + def test_median_pos5(self): + lst = [1, 3, 4, 6, 20] + m = my_utils.get_median(lst) + self.assertEqual(m, 4) + + def test_median_neg5(self): + lst = [-1, -3, -4, -6, -20] + m = my_utils.get_median(lst) + self.assertEqual(m, -4) + + # Test median float + def test_median_posFloat(self): + lst = [0.1, 3.5, 4.5, 800.2] + m = my_utils.get_median(lst) + self.assertEqual(m, 4) + + def test_median_negFloat(self): + lst = [-0.1, -3.5, -4.5, -800.2] + m = my_utils.get_median(lst) + self.assertEqual(m, -4) + + # Assorted median error handling + def test_median_str(self): + lst = ["a", 1, "2"] + self.assertRaises(SystemExit, my_utils.get_median, lst) + + def test_median_empty(self): + lst = [] + self.assertRaises(SystemExit, my_utils.get_median, lst) + + def test_median_none(self): + lst = None + self.assertRaises(SystemExit, my_utils.get_median, lst) + + def test_median_order(self): + lower = [uniform(-10, 0) for _ in range(10)] + upper = [uniform(0, 10) for _ in range(10)] + lst = upper + lower + [0] # Keep bad order for testing, keep 0 too + m = my_utils.get_median(lst) + self.assertEqual(m, 0) + + # Test median large numbers + def test_median_large(self): + lst = [uniform(10, 20) for _ in range(1000000)] + m = my_utils.get_median(lst) + self.assertAlmostEqual(m, 15, 1) + + """get_sd() Tests""" + # Test sd function even + def test_sd_pos4(self): + lst = [0, 0, 2, 2] + sd = my_utils.get_sd(lst) + self.assertAlmostEqual(sd, 1) + + def test_sd_neg4(self): + lst = [0, 0, -2, -2] + sd = my_utils.get_sd(lst) + self.assertAlmostEqual(sd, 1) + + # Test sd function float + def test_sd_Float(self): + lst = [-2.45, 0, 2.449] + sd = my_utils.get_sd(lst) + self.assertAlmostEqual(sd, 2, 4) + + # Assorted sd error handling + def test_sd_str(self): + lst = ["a", 1, 2] + self.assertRaises(SystemExit, my_utils.get_sd, lst) + + def test_sd_empty(self): + lst = [] + self.assertRaises(SystemExit, my_utils.get_sd, lst) + + def test_sd_none(self): + lst = None + self.assertRaises(SystemExit, my_utils.get_sd, lst) + + # Test sd function large numbers + def test_sd_large(self): + lst = [gauss(0, 1) for _ in range(100000)] + sd = my_utils.get_sd(lst) + self.assertAlmostEqual(sd, 1, 2) + + """get_column() Tests""" + def test_col_noFile(self): + self.assertRaises(SystemExit, my_utils.get_column, + "nonexistent.csv", 0, "Afghanistan") + + def test_col_noCSV(self): + self.assertRaises(SystemExit, my_utils.get_column, + "test_my_utils.py", 0, "Afghanistan") + + def test_col_noInt(self): + self.assertRaises(SystemExit, my_utils.get_column, + "test/test_file.csv", "a", "Afghanistan") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_print_fires.sh b/tests/test_print_fires.sh new file mode 100644 index 0000000..9aa80f9 --- /dev/null +++ b/tests/test_print_fires.sh @@ -0,0 +1,78 @@ +test -e ssshtest || wget -q https://raw.githubusercontent.com/ryanlayer/ssshtest/master/ssshtest +. ssshtest + +run basic_test python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" -res_col 1 +assert_in_stdout "[1990, 1991, 1992, 1993, 1994]" +assert_exit_code 0 + +run default_test python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" +assert_in_stdout "[1990, 1991, 1992, 1993, 1994]" +assert_exit_code 0 + +run test_space python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "American Samoa" -res_col 1 +assert_in_stdout "[1990, 1991, 1992, 1993]" +assert_exit_code 0 + +run test_noFile python3 print_fires.py -file "test_file.csv" -q_col 0 \ + -q_val "American Samoa" -res_col 1 +assert_in_stdout "Error: File 'test_file.csv' not found in current directory." +assert_exit_code 1 + +run test_noCSV python3 print_fires.py -file "test/test_my_utils.py" -q_col 0 \ + -q_val "American Samoa" -res_col 1 +assert_in_stdout "Expected a .csv file, got .py" +assert_exit_code 1 + +run test_noQInt python3 print_fires.py -file "test/test_file.csv" -q_col 'a' \ + -q_val "American Samoa" -res_col 1 +assert_in_stdout "Error: Query column ('a') is not integer." +assert_exit_code 1 + +run test_QLong python3 print_fires.py -file "test/test_file.csv" -q_col 105 \ + -q_val "American Samoa" -res_col 1 +assert_in_stdout "Error: Query column ('105') out of bounds." +assert_exit_code 1 + +run test_noRInt python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "American Samoa" -res_col 'a' +assert_in_stdout "Error: Result column ('a') is not integer." +assert_exit_code 1 + +run test_RLong python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "American Samoa" -res_col 105 +assert_in_stdout "Error: Result column ('105') out of bounds." + +run test_Null python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Soviet Union" -res_col 1 +assert_in_stdout "No results found." +assert_in_stdout '[]' +assert_exit_code 0 + +run test_NoInt python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "test_val" -res_col 1 +assert_in_stdout "Data must be numberic. Found "STRING"." +assert_exit_code 1 + +run test_mean python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" -res_col 1 -stat 'mean' +assert_in_stdout "1992" +assert_exit_code 0 + +run test_median python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" -res_col 7 -stat 'median' +assert_in_stdout "11" +assert_exit_code 0 + +run test_sd python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" -res_col 3 -stat 'sd' +assert_in_stdout "0" +assert_exit_code 0 + +run test_sd python3 print_fires.py -file "test/test_file.csv" -q_col 0 \ + -q_val "Afghanistan" -res_col 3 -stat 'PERFECT MODEL' +assert_in_stdout "Error: Invalid statistic requested 'PERFECT MODEL'." +assert_exit_code 1 +