Repository: pyswmm/Stormwater-Management-Model (language: python)

Issue:
Update GitHub actions runners 
Updates build-test.yml windows to VS 2022 and Ubuntu to 20.04

To solve issues shown in #340, CMakeLists.txt were edited to support [cmake option BUILD_SHARED_LIBS](https://cmake.org/cmake/help/latest/guide/tutorial/Selecting%20Static%20or%20Shared%20Libraries.html). Coupled with [changes to the ci-tools repo](https://github.com/OpenWaterAnalytics/ci-tools/pull/16), this PR will allow developers to optionally use static linking with SWMM libraries.

This update is prompted by discussions in #368



Discussion / hints:

Fix patch:
diff --git a/CMakeFiles/cmake.check_cache b/CMakeFiles/cmake.check_cache
new file mode 100644
index 000000000..3dccd7317
--- /dev/null
+++ b/CMakeFiles/cmake.check_cache
@@ -0,0 +1,1 @@
+# This file is generated by cmake for dependency checking of the CMakeCache.txt file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d3255fbac..087455567 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -49,6 +49,8 @@ set(CONFIG_DIST  "cmake")
 option(BUILD_TESTS "Build component tests (requires Boost)" OFF)
 option(BUILD_DOCS "Build toolkit docs (requires Doxygen)" OFF)
 
+# Added option to statically link libraries to address GitHub Ubuntu 20.04 symbol errors (issue #340)
+option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
 
 # Add project subdirectories
 if(BUILD_DOCS)
diff --git a/src/outfile/CMakeLists.txt b/src/outfile/CMakeLists.txt
index 9b44c476c..11b7e5ce8 100644
--- a/src/outfile/CMakeLists.txt
+++ b/src/outfile/CMakeLists.txt
@@ -18,8 +18,7 @@ set(SWMM_OUT_PUBLIC_HEADERS
 
 
 # the binary output file API
-add_library(swmm-output
-    SHARED
+add_library(swmm-output    
         swmm_output.c
         errormanager.c
 )
diff --git a/src/solver/CMakeLists.txt b/src/solver/CMakeLists.txt
index db0d8636f..e10fa4a9a 100644
--- a/src/solver/CMakeLists.txt
+++ b/src/solver/CMakeLists.txt
@@ -38,8 +38,7 @@ if(BUILD_DEF)
         PROPERTIES_HEADER_FILE_ONLY TRUE
     )
 
-    add_library(swmm5
-        SHARED
+    add_library(swmm5        
             ${SWMM_SOURCES}
             ${PROJECT_SOURCE_DIR/bindings/swmm5.def}
             $<TARGET_OBJECTS:shared_objs>
@@ -47,8 +46,7 @@ if(BUILD_DEF)
 
 else()
     # Performs standard library build
-    add_library(swmm5
-        SHARED
+    add_library(swmm5        
             ${SWMM_SOURCES}
             $<TARGET_OBJECTS:shared_objs>
     )
diff --git a/src/solver/include/toolkit.h b/src/solver/include/toolkit.h
index 84d3ac083..7a0bdf250 100644
--- a/src/solver/include/toolkit.h
+++ b/src/solver/include/toolkit.h
@@ -394,11 +394,12 @@ EXPORT_TOOLKIT int swmm_getNodePollut(int index, SM_NodePollut type, double **po
  @param pollutant_value Pollutant value to set 
  @return Error code
 */
-EXPORT_TOOLKIT int swmm_setNodePollut(int index, int pollutant_index, double pollutant_value);
+EXPORT_TOOLKIT int swmm_setNodePollut(int index, SM_NodePollut type, int pollutant_index, double pollutant_value);
 
 /**
  @brief Sets pollutant values for a specified node.
  @param index The index of a node
+ @param type The property type code (See @ref SM_NodePollut)
  @param pollutant_index Pollutant index to set
  @param pollutant_value Pollutant value to set 
  @return Error code
diff --git a/src/solver/toolkit.c b/src/solver/toolkit.c
index c5b8103c8..d563dcd88 100644
--- a/src/solver/toolkit.c
+++ b/src/solver/toolkit.c
@@ -15,6 +15,7 @@
 
 #include <math.h>
 #include <time.h>
+#include <stdio.h>
 
 #include "headers.h"
 #include "version.h"
@@ -1909,6 +1910,8 @@ EXPORT_TOOLKIT int swmm_getNodePollut(int index, SM_NodePollut type, double **po
 
     else
     {
+        *length = Nobjects[POLLUT];
+
         switch (type)
         {
             case SM_NODEQUAL:
@@ -1916,35 +1919,37 @@ EXPORT_TOOLKIT int swmm_getNodePollut(int index, SM_NodePollut type, double **po
                 for (p = 0; p < Nobjects[POLLUT]; p++)
                 {
                     result[p] = Node[index].newQual[p];
-                } 
+                }
+                *pollutArray = result;
+            } break;
+            case SM_NODECIN:
+            {
+                for (p=0; p < Nobjects[POLLUT]; p++)
+                {
+                    result[p] = Node[index].inQual[p];
+                }
+                *pollutArray = result;
+            } break;
+            case SM_NODEREACTORC:
+            {
+                for (p=0; p < Nobjects[POLLUT]; p++)
+                {
+                    result[p] = Node[index].reactorQual[p];
+                }
                 *pollutArray = result;
-                *length = Nobjects[POLLUT];
             } break;
-	    case SM_NODECIN:
-	    {
-		for (p=0; p < Nobjects[POLLUT]; p++)
-		{
-		    result[p] = Node[index].inQual[p];
-		} *pollutArray = result;
-	    } break;
-	    case SM_NODEREACTORC:
-	    {
-		for (p=0; p < Nobjects[POLLUT]; p++)
-		{
-		    result[p] = Node[index].reactorQual[p];
-		} *pollutArray = result;
-	    } break;
             default: error_code_index = ERR_API_OUTBOUNDS; break;
         }
     }
     return error_getCode(error_code_index);
 }
 
-EXPORT_TOOLKIT int swmm_setNodePollut(int index, int pollutant_index, double pollutant_value)
+EXPORT_TOOLKIT int swmm_setNodePollut(int index, SM_NodePollut type, int pollutant_index, double pollutant_value)
 ///
 /// Input:   index = Index of desired ID
+///          type = SM_NODEQUAL - Set's node's qual and allows accounting for loss and mixing calculation
 ///          pollutant_index = Index of desired polluant
-//           pollutant_value = Value of the pollutant
+///          pollutant_value = Value of the pollutant
 /// Return:  API Error
 /// Purpose: Set pollutant concentration in nodes at the current time step
 {
@@ -1968,8 +1973,16 @@ EXPORT_TOOLKIT int swmm_setNodePollut(int index, int pollutant_index, double pol
 		}
 		if (pollutant_index <= Nobjects[POLLUT])
 		{
-			Node[index].extQual[pollutant_index] = pollutant_value;
-	    		Node[index].extPollutFlag[pollutant_index] = 1;
+            switch(type)
+            {
+                case SM_NODEQUAL:
+                {
+                    Node[index].extQual[pollutant_index] = pollutant_value;
+	    		    Node[index].extPollutFlag[pollutant_index] = 1;
+                } break;
+                default: error_code_index = ERR_API_OUTBOUNDS; break;
+            }
+			
 		}
 	}
 	return error_getCode(error_code_index);
@@ -2052,6 +2065,8 @@ EXPORT_TOOLKIT int swmm_getLinkPollut(int index, SM_LinkPollut type, double **po
 
     else
     {
+        *length = Nobjects[POLLUT];
+
         switch (type)
         {
             case SM_LINKQUAL:
@@ -2059,9 +2074,8 @@ EXPORT_TOOLKIT int swmm_getLinkPollut(int index, SM_LinkPollut type, double **po
                 for (p = 0; p < Nobjects[POLLUT]; p++)
                 {
                     result[p] = Link[index].newQual[p];
-                } 
+                }
                 *pollutArray = result;
-                *length = Nobjects[POLLUT];
             } break;
             case SM_TOTALLOAD:
             {
@@ -2074,7 +2088,14 @@ EXPORT_TOOLKIT int swmm_getLinkPollut(int index, SM_LinkPollut type, double **po
                     }
                 } 
                 *pollutArray = result;
-                *length = Nobjects[POLLUT];
+            } break;
+            case SM_LINKREACTORC:
+            {
+                for (p = 0; p < Nobjects[POLLUT]; p++)
+                {
+                    result[p] = Link[index].reactorQual[p];
+                }
+                *pollutArray = result;
             } break;
             default: error_code_index = ERR_API_OUTBOUNDS; break;
         } 
@@ -2089,18 +2110,18 @@ EXPORT_TOOLKIT int swmm_setLinkPollut(int index, SM_LinkPollut type, int polluta
 /// 	    type = SM_LINKQUAL - Sets link's qual and allows accounting for loss and mixing calculation
 ///         pollutant_index = index of pollutant to set
 ///         pollutant_value = concentration to set
-///  Output: API error
+///  Return: API error
 ///  Purponse: Set pollutant concentration in links 
 {
 	int error_code_index = 0;
-
+    
 	// Check if Open
 	if(swmm_IsOpenFlag() == FALSE)
 	{
 	    error_code_index = ERR_API_INPUTNOTOPEN;
 	}
 	// Check if object index is within bounds
-	else if (index < 0 || index >= Nobjects[NODE])
+	else if (index < 0 || index >= Nobjects[LINK])
 	{
 	    error_code_index = ERR_API_OBJECT_INDEX;
 	}


Test patch:
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index c9c5b5554..41e7c5020 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -20,15 +20,15 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [windows-2016, ubuntu-16.04, macos-10.15]
+        os: [windows-2022, ubuntu-20.04, macos-10.15]
         requirements: [requirements-swmm.txt]
         include:
-          - os: windows-2016
+          - os: windows-2022
             sys_pkgs: choco install boost-msvc-14.1
-            build_unit_test: make.cmd -t -g "Visual Studio 15 2017 Win64"
-            build_reg_test:  make.cmd -g "Visual Studio 15 2017 Win64"
+            build_unit_test: make.cmd /g "Visual Studio 17 2022" /A "x64" /t
+            build_reg_test: make.cmd /g "Visual Studio 17 2022" /A "x64"
             before_reg_test: before-nrtest.cmd
-            run_reg_test:    run-nrtests.cmd
+            run_reg_test: run-nrtests.cmd
             build_id: "%GITHUB_RUN_ID%_%GITHUB_RUN_NUMBER%"
             experimental: false
             artifacts_ext: zip
@@ -38,12 +38,13 @@ jobs:
                 shell: cmd
                 working-directory: ./ci-tools/windows
 
-          - os: ubuntu-16.04
+          - os: ubuntu-20.04
             sys_pkgs: sudo apt install libboost-dev libboost-all-dev
-            build_unit_test: make.sh -t -g "Unix Makefiles"
-            build_reg_test:  make.sh -g "Unix Makefiles"
+            # Statically link libraries with -s switch to address GitHub Ubuntu 20.04 symbol errors (issue #340)
+            build_unit_test: make.sh -s -t -g "Unix Makefiles"
+            build_reg_test: make.sh -s -g "Unix Makefiles"
             before_reg_test: before-nrtest.sh
-            run_reg_test:    run-nrtests.sh
+            run_reg_test: run-nrtests.sh
             build_id: ${GITHUB_RUN_ID}_${GITHUB_RUN_NUMBER}
             experimental: true
             artifacts_ext: tar.gz
@@ -56,9 +57,9 @@ jobs:
           - os: macos-10.15
             sys_pkgs: brew install libomp boost
             build_unit_test: make.zsh -t -g "Xcode"
-            build_reg_test:  make.zsh -g "Xcode"
+            build_reg_test: make.zsh -g "Xcode"
             before_reg_test: before-nrtest.zsh
-            run_reg_test:    run-nrtests.zsh
+            run_reg_test: run-nrtests.zsh
             build_id: ${GITHUB_RUN_ID}_${GITHUB_RUN_NUMBER}
             experimental: true
             artifacts_ext: tar.gz
@@ -80,7 +81,6 @@ jobs:
       BUILD_HOME: build
       TEST_HOME: nrtests
 
-
     steps:
       - name: Checkout repo
         uses: actions/checkout@v2
@@ -114,7 +114,7 @@ jobs:
       - name: Before reg test
         env:
           NRTESTS_URL: https://github.com/OpenWaterAnalytics/swmm-nrtestsuite
-          BENCHMARK_TAG: v1.0.4-dev
+          BENCHMARK_TAG: v2.0.1
         run: ./${{ matrix.before_reg_test }} ${{ env.BENCHMARK_TAG }}
 
       - name: Run reg test
diff --git a/tests/solver/test_pollut.cpp b/tests/solver/test_pollut.cpp
index 9b879d977..e1c978f95 100644
--- a/tests/solver/test_pollut.cpp
+++ b/tests/solver/test_pollut.cpp
@@ -60,6 +60,8 @@ BOOST_FIXTURE_TEST_CASE(get_pollut_values, FixtureBeforeStep){
         {
             // subcatchment buildup
             error = swmm_getSubcatchPollut(subc_ind, SM_BUILDUP, &buildup_array, &length);
+            // Check length is set correctly
+            BOOST_CHECK(length == 2);
             BOOST_REQUIRE(error == ERR_NONE);
             BOOST_CHECK_SMALL(buildup_array[TSS] - 31.906912, 0.0001);
             BOOST_CHECK_SMALL(buildup_array[Lead] - 0.0, 0.0001);
@@ -164,8 +166,11 @@ BOOST_FIXTURE_TEST_CASE(get_node_pollutant_values_cin, FixtureBeforeStep_Pollut_
     step_ind = 0;
     do
     {
-
+       
+    // Get inflow concentration
 	error = swmm_getNodePollut(1, SM_NODECIN, &node_qual, &length);
+    // Check length is set correctly
+    BOOST_CHECK(length == 1);
 	BOOST_REQUIRE(error == ERR_NONE);
 
 	// Check for constant influent
@@ -182,7 +187,7 @@ BOOST_FIXTURE_TEST_CASE(get_node_pollutant_values_cin, FixtureBeforeStep_Pollut_
     swmm_end();
 }
 
-// Testing Reactor Concentration
+// Testing Reactor Concentration in Node
 BOOST_FIXTURE_TEST_CASE(get_node_reactor_pollutant, FixtureBeforeStep_Pollut_Node){
 
     int error, step_ind;
@@ -198,12 +203,14 @@ BOOST_FIXTURE_TEST_CASE(get_node_reactor_pollutant, FixtureBeforeStep_Pollut_Nod
     step_ind = 0;
     do
     {
-	// Check for steady state after 1000 steps.
+    // Check for steady state after 1000 steps.
 	// 1000 is a aribitarly long time duration, it can be any value as long
 	// the system reaches a steady state
 
 	// Get reactor concentration
 	error = swmm_getNodePollut(1, SM_NODEREACTORC, &new_qual, &length);
+    // Check length is set correctly
+    BOOST_CHECK(length == 1);
 	BOOST_REQUIRE(error == ERR_NONE);
 
 	if (step_ind > 1000)
@@ -221,6 +228,46 @@ BOOST_FIXTURE_TEST_CASE(get_node_reactor_pollutant, FixtureBeforeStep_Pollut_Nod
     swmm_end();
 }
 
+// Testing Reactor Concentration in Link
+BOOST_FIXTURE_TEST_CASE(get_link_reactor_pollutant, FixtureBeforeStep_Pollut_Link){
+
+    int error, step_ind;
+    double* old_qual;
+    double* new_qual;
+    double elapsedTime = 0.0;
+    double total_pollutant = 0.0;
+    int length;
+
+    // Pollutant IDs
+    int P1 = 0;
+
+    step_ind = 0;
+    do
+    {
+    // Check for steady state after 1000 steps.
+    // 1000 is a aribitarly long time duration, it can be any value as long
+    // the system reaches a steady state
+
+    // Get reactor concentration
+    error = swmm_getLinkPollut(1, SM_LINKREACTORC, &new_qual, &length);
+    // Check length is set correctly
+    BOOST_CHECK(length == 1);
+    BOOST_REQUIRE(error == ERR_NONE);
+
+    if (step_ind > 1500)
+    {
+        BOOST_CHECK_CLOSE(old_qual[P1], new_qual[P1], 0.001);
+    }
+
+    old_qual = new_qual;
+
+        // Route Model Forward
+        error = swmm_step(&elapsedTime);
+        step_ind+=1;
+    }while (elapsedTime != 0 && !error);
+    BOOST_REQUIRE(error == ERR_NONE);
+    swmm_end();
+}
 
 // Testing Pollutant Setter - Node - Cumulative and mass balance
 BOOST_FIXTURE_TEST_CASE(set_node_pollutant_cumulative_values, FixtureBeforeStep_Pollut_Node){
@@ -238,7 +285,7 @@ BOOST_FIXTURE_TEST_CASE(set_node_pollutant_cumulative_values, FixtureBeforeStep_
     do
     {
 	// Set pollutant
-	error = swmm_setNodePollut(1, P1, 0);
+	error = swmm_setNodePollut(1, SM_NODEQUAL, P1, 0);
 	BOOST_REQUIRE(error == ERR_NONE);
         // Route Model Forward
         error = swmm_step(&elapsedTime);
@@ -273,7 +320,7 @@ BOOST_FIXTURE_TEST_CASE(set_node_pollutant_stepwise_values, FixtureBeforeStep_Po
     do
     {
 	// Set pollutant
-	error = swmm_setNodePollut(1, P1, 1.234);
+	error = swmm_setNodePollut(1, SM_NODEQUAL, P1, 1.234);
 	BOOST_REQUIRE(error == ERR_NONE);
 
 	// Route Model Forward
@@ -310,7 +357,7 @@ BOOST_FIXTURE_TEST_CASE(set_node_pollutant_stepwise_values_2, FixtureBeforeStep_
     do
     {
 	// Set pollutant
-	error = swmm_setNodePollut(1, P1, 50.0);
+	error = swmm_setNodePollut(1, SM_NODEQUAL, P1, 50.0);
 	BOOST_REQUIRE(error == ERR_NONE);
 
 	// Route Model Forward