From cd3eca0102ced446dcaff229c2e9c1a766572819 Mon Sep 17 00:00:00 2001 From: Jason Chan Date: Mon, 31 Aug 2026 09:03:23 -0500 Subject: [PATCH 1/7] feat: set tests for each Solver as a test parameter --- tests/integration/SolverIntegrationTest.m | 498 +++++++++++----------- 1 file changed, 247 insertions(+), 251 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index e5936eb..519abc3 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -32,8 +32,8 @@ properties (SetAccess = private) testNumber % Numerical identifiers for the test cases. - inputSets % Input sets independently retained by the test class. - solvers % OpenSTREAM solver objects. + inputSet % Input sets independently retained by the test class. + solver % OpenSTREAM solver objects. testFolder % Absolute path to the folder containing this test class. testRoot % Root folder containing all automated tests. openstreamRoot % Absolute path to the OpenSTREAM repository root. @@ -43,10 +43,63 @@ end + properties (ClassSetupParameter) + % Define the test cases. + % + % Each structure contains: + % Number Numerical identifier used by the test class. + % ModelID Physical-model input-set identifier. + % OptionsID Numerical-option input-set identifier. + % GeometryID Geometry input-set identifier. + % BoundaryCondition Boundary-condition filename without the .inp extension. + % Constructor Function handle to the corresponding solver constructor. + % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. + + case_option = struct( ... + Mixture = ... + struct( ... + Number = 1, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.Mixture.MixtureSolver, ... + MixtureCase = 0), ... + TwoFluid = ... + struct( ... + Number = 2, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.TwoFluid.TwoFluidSolver, ... + MixtureCase = 1), ... + ThreeField = ... + struct( ... + Number = 3, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.ThreeField.ThreeFieldSolver, ... + MixtureCase = 1), ... + FourField = ... + struct( ... + Number = 4, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.FourField.FourFieldSolver, ... + MixtureCase = 1) ... + ); + + end + methods (TestClassSetup) - function constructAndSolveTestCases(testCase) - % CONSTRUCTANDSOLVETESTCASES + function constructAndSolveTestCase(testCase, case_option) + % CONSTRUCTANDSOLVETESTCASE % % Shared setup executed once before the individual test methods. % @@ -55,7 +108,7 @@ function constructAndSolveTestCases(testCase) % 1. Determines the repository paths. % 2. Adds the OpenSTREAM root to the MATLAB path. % 3. Recreates the output directory. - % 4. Defines the solver cases. + % 4. Defines the solver case. % 5. Constructs and solves every solver object. % % The solved objects are retained in testCase.solvers and @@ -80,7 +133,7 @@ function constructAndSolveTestCases(testCase) testCase.inputFolder = fullfile(testCase.openstreamRoot,'inputs'); % Define a dedicated output folder for this test class. - testCase.outputFolder = fullfile(testCase.testFolder,'outputs'); + testCase.outputFolder = fullfile(testCase.testFolder,'outputs',string(case_option.Number)); % Define the approved-reference folder. testCase.referenceFolder = fullfile(testCase.testFolder,'references'); @@ -111,74 +164,23 @@ function constructAndSolveTestCases(testCase) % Import the OpenSTREAM packages. import Inputs.* - import Solvers.* - import Solvers.Mixture.* - import Solvers.TwoFluid.* - import Solvers.ThreeField.* - import Solvers.FourField.* - - % Define the test cases. - % - % Each structure contains: - % Number Numerical identifier used by the test class. - % ModelID Physical-model input-set identifier. - % OptionsID Numerical-option input-set identifier. - % GeometryID Geometry input-set identifier. - % BoundaryCondition Boundary-condition filename without the .inp extension. - % Constructor Function handle to the corresponding solver constructor. - % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. - - cases = [ - struct( ... - Number = 1, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @MixtureSolver, ... - MixtureCase = 0); - - struct( ... - Number = 2, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @TwoFluidSolver, ... - MixtureCase = 1); - - struct( ... - Number = 3, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @ThreeFieldSolver, ... - MixtureCase = 1); - - struct( ... - Number = 4, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @FourFieldSolver, ... - MixtureCase = 1) - ]; - - % Preallocate the shared test properties. - - numberOfCases = numel(cases); - testCase.testNumber = zeros(1,numberOfCases); - testCase.inputSets = cell(1,numberOfCases); - testCase.solvers = cell(1,numberOfCases); - - % Construct and solve every test case. - - for caseIndex = 1:numberOfCases + % import Solvers.* + % import Solvers.Mixture.* + % import Solvers.TwoFluid.* + % import Solvers.ThreeField.* + % import Solvers.FourField.* + + % % Preallocate the shared test properties. + % + % numberOfCases = numel(cases); + % testCase.testNumber = zeros(1,numberOfCases); + % testCase.inputSets = cell(1,numberOfCases); + % testCase.solvers = cell(1,numberOfCases); + + % Construct and solve test case. % Store the test identifier. - testCase.testNumber(caseIndex) = cases(caseIndex).Number; + testCase.testNumber = case_option.Number; % Preserve the current MATLAB warning configuration. warningState = warning; @@ -194,35 +196,37 @@ function constructAndSolveTestCases(testCase) % Construct the input set. inputSet = InputSet( ... modelFilePath = fullfile(testCase.inputFolder,'models.inp'), ... - modelID = cases(caseIndex).ModelID, ... + modelID = case_option.ModelID, ... optionsFilePath = fullfile(testCase.inputFolder,'options.inp'), ... - optionsID = cases(caseIndex).OptionsID, ... + optionsID = case_option.OptionsID, ... geometryFilePath = fullfile(testCase.inputFolder,'geom.inp'), ... - geometryID = cases(caseIndex).GeometryID, ... - bcFilePath = fullfile(testCase.inputFolder,cases(caseIndex).BoundaryCondition + ".inp"), ... + geometryID = case_option.GeometryID, ... + bcFilePath = fullfile(testCase.inputFolder,case_option.BoundaryCondition + ".inp"), ... sessionParentDir = testCase.outputFolder, ... - overwriteSessionFiles = true, ... + overwriteSessionFiles = false, ... LOGMODE = 'BOTH'); - testCase.inputSets{caseIndex} = inputSet; + testCase.inputSet = inputSet; % Clearing the cleanup object restores the original warning % configuration immediately after successful construction. clear warningCleanup % Construct the solver object. - if cases(caseIndex).MixtureCase == 0 - % The mixture solver does not require an existing mixture solution. - testCase.solvers{caseIndex} = cases(caseIndex).Constructor(testCase.inputSets{caseIndex}); - else - % Retrieve the solved mixture object used to initialize the advanced solver. - mixtureSolver = testCase.solvers{cases(caseIndex).MixtureCase}; - % Construct the advanced solver using the common mixture solution. - testCase.solvers{caseIndex} = cases(caseIndex).Constructor(testCase.inputSets{caseIndex},mixtureSolver); - end + % if case_option.MixtureCase == 0 + % % The mixture solver does not require an existing mixture solution. + % testCase.solver= case_option.Constructor(testCase.inputSet); + % else + % % Retrieve the solved mixture object used to initialize the advanced solver. + % mixtureSolver = testCase.solver{case_option.MixtureCase}; + % % Construct the advanced solver using the common mixture solution. + % testCase.solver = case_option.Constructor(testCase.inputSet,mixtureSolver); + % end + + testCase.solver= case_option.Constructor(testCase.inputSet); % Solve the flow fields. - testCase.solvers{caseIndex}.solve(); - end + testCase.solver.solve(); + end @@ -233,24 +237,20 @@ function constructAndSolveTestCases(testCase) function solverStates(testCase) % SOLVERSTATES % - % Verify that every solver reaches the expected converged state. - - for caseIndex = 1:numel(testCase.solvers) - - % Retrieve the calculated solver object. - results = testCase.solvers{caseIndex}; + % Verify that the solver reaches the expected converged state. - % Obtain the short solver-class name for the test message. - solverName = testCase.getSolverName(results); + % Retrieve the calculated solver object. + result = testCase.solver; - % Verify the expected solver state. - testCase.verifyEqual( ... - results.STATE, ... - Solvers.SolverState.INITIALSTEPCONVERGED, ... - solverName + ... - ": The solver did not reach the expected converged state."); + % Obtain the short solver-class name for the test message. + solverName = testCase.getSolverName(result); - end + % Verify the expected solver state. + testCase.verifyEqual( ... + result.STATE, ... + Solvers.SolverState.INITIALSTEPCONVERGED, ... + solverName + ... + ": The solver did not reach the expected converged state."); end @@ -260,16 +260,20 @@ function mixtureBoundaryConditions(testCase) % Verify that the first stored physical state satisfies the prescribed % inlet boundary conditions without modification. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; - + % Assume the mixture-solver case. + % testCase.assumeEqual( ... + % class(testCase.solver), ... + % 'Solvers.Mixture.MixtureSolver', ... + % "this test is only for the MixtureSolver case.") + + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end + % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -280,7 +284,7 @@ function mixtureBoundaryConditions(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSets{mixtureCaseIndex}; + suppliedInputSet = testCase.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -317,15 +321,13 @@ function mixtureEnergyBalance(testCase) % Verify that the first stored physical state satisfies the prescribed % steady-state energy balance. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -337,7 +339,7 @@ function mixtureEnergyBalance(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSets{mixtureCaseIndex}; + suppliedInputSet = testCase.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -370,15 +372,13 @@ function mixtureInitializationTransfer(testCase) % initialization state are transferred unchanged to the first stored % physical state. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -461,10 +461,10 @@ function saveLoadRoundTrip(testCase) % % The solver is stored using the variable name "solver". - for solverCell = testCase.solvers + for solverCell = testCase.solver % Retrieve the original solver object. - original = solverCell{1}; + original = testCase.solver; % Obtain the short solver-class name used in the generated MAT-file name. solverName = testCase.getSolverName(original); @@ -668,128 +668,124 @@ function compareFieldProperty( ... % test. numberOfVerifiedProperties = 0; - for caseIndex = 1:numel(testCase.solvers) - - % Retrieve the calculated solver object. - results = testCase.solvers{caseIndex}; - - % Load the corresponding approved reference solver. - references = testCase.loadReference(results); - - % Obtain the short solver-class name for diagnostic messages. - solverName = testCase.getSolverName(results); - - % Count the applicable field quantities verified for this solver. - numberOfVerifiedFields = 0; - - for fieldPath = fieldPaths - - % Retrieve the calculated field object at the first stored - % physical state. - [resultField,resultFieldExists] = ... - testCase.getFieldByPath( ... - results, ... - fieldPath); - - % Retrieve the corresponding reference field object. - [referenceField,referenceFieldExists] = ... - testCase.getFieldByPath( ... - references, ... - fieldPath); - - % Verify that the calculated and reference solver structures - % expose the same field paths. - testCase.assertEqual( ... - resultFieldExists, ... - referenceFieldExists, ... - solverName + ... - ": Field path '" + fieldPath + ... - "' is not consistently available."); - - % Skip field paths that do not apply to the selected solver. - if ~resultFieldExists - continue - end - - % Retrieve the requested calculated quantity. The quantity may - % be implemented as a property or as a zero-input method. - [resultValue,resultValueExists] = ... - testCase.getFieldValue( ... - resultField, ... - propertyName); - - % Retrieve the corresponding reference quantity. - [referenceValue,referenceValueExists] = ... - testCase.getFieldValue( ... - referenceField, ... - propertyName); - - % Verify that the calculated and reference field interfaces - % expose the requested quantity consistently. - testCase.assertEqual( ... - resultValueExists, ... - referenceValueExists, ... - solverName + ... - ": Quantity '" + propertyName + ... - "' is not consistently available in field '" + ... - fieldPath + "'."); - - % Skip fields that do not expose the requested quantity. - if ~resultValueExists - continue - end - - % Verify that the calculated quantity is not empty. - testCase.assertNotEmpty( ... - resultValue, ... - solverName + ... - ": Field '" + fieldPath + ... - "' has an empty " + ... - propertyDescription + " array."); - - % Verify that the reference quantity is not empty. - testCase.assertNotEmpty( ... - referenceValue, ... - solverName + ... - ": Reference field '" + fieldPath + ... - "' has an empty " + ... - propertyDescription + " array."); - - % Compare the calculated and approved reference values. - diagnostic = sprintf( ... - '%s: %s %s mismatch using RelTol = %.1e.', ... - char(solverName), ... - char(fieldPath), ... - char(propertyDescription), ... - relativeTolerance); - - testCase.verifyEqual( ... - resultValue, ... - referenceValue, ... - diagnostic, ... - RelTol = relativeTolerance); - - % Record the completed comparison. - numberOfVerifiedFields = ... - numberOfVerifiedFields + 1; - - numberOfVerifiedProperties = ... - numberOfVerifiedProperties + 1; + % Retrieve the calculated solver object. + results = testCase.solver; + + % Load the corresponding approved reference solver. + references = testCase.loadReference(results); + + % Obtain the short solver-class name for diagnostic messages. + solverName = testCase.getSolverName(results); + + % Count the applicable field quantities verified for this solver. + numberOfVerifiedFields = 0; + + for fieldPath = fieldPaths + + % Retrieve the calculated field object at the first stored + % physical state. + [resultField,resultFieldExists] = ... + testCase.getFieldByPath( ... + results, ... + fieldPath); + + % Retrieve the corresponding reference field object. + [referenceField,referenceFieldExists] = ... + testCase.getFieldByPath( ... + references, ... + fieldPath); + + % Verify that the calculated and reference solver structures + % expose the same field paths. + testCase.assertEqual( ... + resultFieldExists, ... + referenceFieldExists, ... + solverName + ... + ": Field path '" + fieldPath + ... + "' is not consistently available."); + % Skip field paths that do not apply to the selected solver. + if ~resultFieldExists + continue end - % Ensure that at least one applicable field quantity was verified - % for the selected solver. - testCase.verifyGreaterThan( ... - numberOfVerifiedFields, ... - 0, ... + % Retrieve the requested calculated quantity. The quantity may + % be implemented as a property or as a zero-input method. + [resultValue,resultValueExists] = ... + testCase.getFieldValue( ... + resultField, ... + propertyName); + + % Retrieve the corresponding reference quantity. + [referenceValue,referenceValueExists] = ... + testCase.getFieldValue( ... + referenceField, ... + propertyName); + + % Verify that the calculated and reference field interfaces + % expose the requested quantity consistently. + testCase.assertEqual( ... + resultValueExists, ... + referenceValueExists, ... solverName + ... - ": No applicable " + ... - propertyDescription + ... - " field was found."); + ": Quantity '" + propertyName + ... + "' is not consistently available in field '" + ... + fieldPath + "'."); + + % Skip fields that do not expose the requested quantity. + if ~resultValueExists + continue + end + + % Verify that the calculated quantity is not empty. + testCase.assertNotEmpty( ... + resultValue, ... + solverName + ... + ": Field '" + fieldPath + ... + "' has an empty " + ... + propertyDescription + " array."); + + % Verify that the reference quantity is not empty. + testCase.assertNotEmpty( ... + referenceValue, ... + solverName + ... + ": Reference field '" + fieldPath + ... + "' has an empty " + ... + propertyDescription + " array."); + + % Compare the calculated and approved reference values. + diagnostic = sprintf( ... + '%s: %s %s mismatch using RelTol = %.1e.', ... + char(solverName), ... + char(fieldPath), ... + char(propertyDescription), ... + relativeTolerance); + + testCase.verifyEqual( ... + resultValue, ... + referenceValue, ... + diagnostic, ... + RelTol = relativeTolerance); + + % Record the completed comparison. + numberOfVerifiedFields = ... + numberOfVerifiedFields + 1; + + numberOfVerifiedProperties = ... + numberOfVerifiedProperties + 1; end + % Ensure that at least one applicable field quantity was verified + % for the selected solver. + testCase.verifyGreaterThan( ... + numberOfVerifiedFields, ... + 0, ... + solverName + ... + ": No applicable " + ... + propertyDescription + ... + " field was found."); + % Prevent the complete test from passing without comparing any values. testCase.verifyGreaterThan( ... numberOfVerifiedProperties, ... From 71377ea05e4e190534e8da448611ffde17093824 Mon Sep 17 00:00:00 2001 From: lecorrejm Date: Sun, 13 Sep 2026 13:30:25 +0200 Subject: [PATCH 2/7] refactor(tests): Cleanup SolverIntegrationTests and add descriptive folder name --- tests/integration/SolverIntegrationTest.m | 172 +++++++++------------- 1 file changed, 70 insertions(+), 102 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index 519abc3..3e1576a 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -19,8 +19,11 @@ % retaining their class and solver state. % % The tests use the TUTORIAL1 model, numerical-option, geometry, and - % boundary-condition input sets. The two-fluid, three-field, and - % four-field solvers reuse the converged mixture-solver solution. + % boundary-condition input sets. + % + % Each solver framework is tested in an independent class-setup + % Parameterization. The two-fluid, three-field, and four-field + % constructors create and solve their required mixture solution. % % Approved reference solver objects are expected in: % tests/integration/references// @@ -53,52 +56,51 @@ % GeometryID Geometry input-set identifier. % BoundaryCondition Boundary-condition filename without the .inp extension. % Constructor Function handle to the corresponding solver constructor. - % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. - case_option = struct( ... + caseOption = struct( ... Mixture = ... struct( ... + Name = "Mixture", ... Number = 1, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.Mixture.MixtureSolver, ... - MixtureCase = 0), ... + Constructor = @Solvers.Mixture.MixtureSolver), ... TwoFluid = ... struct( ... + Name = "TwoFluid", ... Number = 2, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.TwoFluid.TwoFluidSolver, ... - MixtureCase = 1), ... + Constructor = @Solvers.TwoFluid.TwoFluidSolver), ... ThreeField = ... struct( ... + Name = "ThreeField", ... Number = 3, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.ThreeField.ThreeFieldSolver, ... - MixtureCase = 1), ... + Constructor = @Solvers.ThreeField.ThreeFieldSolver), ... FourField = ... struct( ... + Name = "FourField", ... Number = 4, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.FourField.FourFieldSolver, ... - MixtureCase = 1) ... + Constructor = @Solvers.FourField.FourFieldSolver) ... ); end methods (TestClassSetup) - function constructAndSolveTestCase(testCase, case_option) + function constructAndSolveTestCase(testCase, caseOption) % CONSTRUCTANDSOLVETESTCASE % % Shared setup executed once before the individual test methods. @@ -109,10 +111,10 @@ function constructAndSolveTestCase(testCase, case_option) % 2. Adds the OpenSTREAM root to the MATLAB path. % 3. Recreates the output directory. % 4. Defines the solver case. - % 5. Constructs and solves every solver object. + % 5. Constructs and solves the selected solver object. % - % The solved objects are retained in testCase.solvers and - % reused by all test methods. + % The solved object is retained in testCase.solver and reused by all + % test methods for the current class-setup parameterization. % Determine the test and repository paths. @@ -133,7 +135,7 @@ function constructAndSolveTestCase(testCase, case_option) testCase.inputFolder = fullfile(testCase.openstreamRoot,'inputs'); % Define a dedicated output folder for this test class. - testCase.outputFolder = fullfile(testCase.testFolder,'outputs',string(case_option.Number)); + testCase.outputFolder = fullfile(testCase.testFolder,'outputs',caseOption.Name); % Define the approved-reference folder. testCase.referenceFolder = fullfile(testCase.testFolder,'references'); @@ -162,25 +164,13 @@ function constructAndSolveTestCase(testCase, case_option) % Create a clean output directory for this test run. mkdir(testCase.outputFolder); - % Import the OpenSTREAM packages. + % Import the OpenSTREAM Inputs package. import Inputs.* - % import Solvers.* - % import Solvers.Mixture.* - % import Solvers.TwoFluid.* - % import Solvers.ThreeField.* - % import Solvers.FourField.* - - % % Preallocate the shared test properties. - % - % numberOfCases = numel(cases); - % testCase.testNumber = zeros(1,numberOfCases); - % testCase.inputSets = cell(1,numberOfCases); - % testCase.solvers = cell(1,numberOfCases); % Construct and solve test case. % Store the test identifier. - testCase.testNumber = case_option.Number; + testCase.testNumber = caseOption.Number; % Preserve the current MATLAB warning configuration. warningState = warning; @@ -196,12 +186,12 @@ function constructAndSolveTestCase(testCase, case_option) % Construct the input set. inputSet = InputSet( ... modelFilePath = fullfile(testCase.inputFolder,'models.inp'), ... - modelID = case_option.ModelID, ... + modelID = caseOption.ModelID, ... optionsFilePath = fullfile(testCase.inputFolder,'options.inp'), ... - optionsID = case_option.OptionsID, ... + optionsID = caseOption.OptionsID, ... geometryFilePath = fullfile(testCase.inputFolder,'geom.inp'), ... - geometryID = case_option.GeometryID, ... - bcFilePath = fullfile(testCase.inputFolder,case_option.BoundaryCondition + ".inp"), ... + geometryID = caseOption.GeometryID, ... + bcFilePath = fullfile(testCase.inputFolder,caseOption.BoundaryCondition + ".inp"), ... sessionParentDir = testCase.outputFolder, ... overwriteSessionFiles = false, ... LOGMODE = 'BOTH'); @@ -212,17 +202,7 @@ function constructAndSolveTestCase(testCase, case_option) clear warningCleanup % Construct the solver object. - % if case_option.MixtureCase == 0 - % % The mixture solver does not require an existing mixture solution. - % testCase.solver= case_option.Constructor(testCase.inputSet); - % else - % % Retrieve the solved mixture object used to initialize the advanced solver. - % mixtureSolver = testCase.solver{case_option.MixtureCase}; - % % Construct the advanced solver using the common mixture solution. - % testCase.solver = case_option.Constructor(testCase.inputSet,mixtureSolver); - % end - - testCase.solver= case_option.Constructor(testCase.inputSet); + testCase.solver= caseOption.Constructor(testCase.inputSet); % Solve the flow fields. testCase.solver.solve(); @@ -284,7 +264,7 @@ function mixtureBoundaryConditions(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSet; + suppliedInputSet = results.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -339,7 +319,7 @@ function mixtureEnergyBalance(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSet; + suppliedInputSet = results.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -452,71 +432,59 @@ function fieldEnthalpies(testCase) function saveLoadRoundTrip(testCase) % SAVELOADROUNDTRIP % - % Verify that each solver object: - % - % 1. Can be saved to a MAT-file. - % 2. Can be loaded from that MAT-file. - % 3. Retains the solver class. - % 4. Retains the solver state. - % - % The solver is stored using the variable name "solver". - - for solverCell = testCase.solver - - % Retrieve the original solver object. - original = testCase.solver; - - % Obtain the short solver-class name used in the generated MAT-file name. - solverName = testCase.getSolverName(original); + % Verify that the solver object can be saved and reloaded while + % retaining its class and solver state. - % Save the solver object. + % Retrieve the original solver object. + original = testCase.solver; - original.save( ... - 'name', 'solver', ... - 'showpath', false); + % Obtain the short solver-class name used in the generated MAT-file name. + solverName = testCase.getSolverName(original); - % Construct the expected saved-file path. - fileName = fullfile( ... - original.inputSet.session.parentDir, ... - original.inputSet.session.dirName, ... - original.inputSet.session.name + ... - "_" + solverName + ".mat"); + % Save the solver object. + original.save( ... + 'name', 'solver', ... + 'showpath', false); - % Confirm that the MAT-file was created. - testCase.assertTrue( ... - isfile(fileName), ... - solverName + ... - ": Saved solver file was not created."); - - % Reload the solver object. + % Construct the expected saved-file path. + fileName = fullfile( ... + original.inputSet.session.parentDir, ... + original.inputSet.session.dirName, ... + original.inputSet.session.name + ... + "_" + solverName + ".mat"); - savedData = load(fileName); + % Confirm that the MAT-file was created. + testCase.assertTrue( ... + isfile(fileName), ... + solverName + ... + ": Saved solver file was not created."); - % Confirm that the selected variable name was stored in the MAT-file. - testCase.assertTrue( ... - isfield(savedData,'solver'), ... - solverName + ... - ": Saved variable 'solver' is missing."); + % Reload the solver object. + savedData = load(fileName); - restored = savedData.solver; + % Confirm that the selected variable name was stored in the MAT-file. + testCase.assertTrue( ... + isfield(savedData,'solver'), ... + solverName + ... + ": Saved variable 'solver' is missing."); - % Verify the restored object. + restored = savedData.solver; - % Verify that the restored object has the same solver class. - testCase.verifyEqual( ... - class(restored), ... - class(original), ... - solverName + ... - ": Restored solver class mismatch."); + % Verify the restored object. - % Verify that the solver state was preserved. - testCase.verifyEqual( ... - restored.STATE, ... - original.STATE, ... - solverName + ... - ": Restored solver state mismatch."); + % Verify that the restored object has the same solver class. + testCase.verifyEqual( ... + class(restored), ... + class(original), ... + solverName + ... + ": Restored solver class mismatch."); - end + % Verify that the solver state was preserved. + testCase.verifyEqual( ... + restored.STATE, ... + original.STATE, ... + solverName + ... + ": Restored solver state mismatch."); end From 259834ca1cc251e2f362db8975d8bad5945df81a Mon Sep 17 00:00:00 2001 From: lecorrejm Date: Sun, 13 Sep 2026 15:18:31 +0200 Subject: [PATCH 3/7] refactor(tests): Cleanup SolverIntegrationTests --- tests/integration/SolverIntegrationTest.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index 3e1576a..3a84e09 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -239,12 +239,6 @@ function mixtureBoundaryConditions(testCase) % % Verify that the first stored physical state satisfies the prescribed % inlet boundary conditions without modification. - - % Assume the mixture-solver case. - % testCase.assumeEqual( ... - % class(testCase.solver), ... - % 'Solvers.Mixture.MixtureSolver', ... - % "this test is only for the MixtureSolver case.") if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") % Retrieve the calculated mixture solver. From 7f393de21e84a739f74b86d991160ae2e361eb81 Mon Sep 17 00:00:00 2001 From: Jason Chan Date: Mon, 31 Aug 2026 09:03:23 -0500 Subject: [PATCH 4/7] feat: set tests for each Solver as a test parameter --- tests/integration/SolverIntegrationTest.m | 498 +++++++++++----------- 1 file changed, 247 insertions(+), 251 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index e5936eb..519abc3 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -32,8 +32,8 @@ properties (SetAccess = private) testNumber % Numerical identifiers for the test cases. - inputSets % Input sets independently retained by the test class. - solvers % OpenSTREAM solver objects. + inputSet % Input sets independently retained by the test class. + solver % OpenSTREAM solver objects. testFolder % Absolute path to the folder containing this test class. testRoot % Root folder containing all automated tests. openstreamRoot % Absolute path to the OpenSTREAM repository root. @@ -43,10 +43,63 @@ end + properties (ClassSetupParameter) + % Define the test cases. + % + % Each structure contains: + % Number Numerical identifier used by the test class. + % ModelID Physical-model input-set identifier. + % OptionsID Numerical-option input-set identifier. + % GeometryID Geometry input-set identifier. + % BoundaryCondition Boundary-condition filename without the .inp extension. + % Constructor Function handle to the corresponding solver constructor. + % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. + + case_option = struct( ... + Mixture = ... + struct( ... + Number = 1, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.Mixture.MixtureSolver, ... + MixtureCase = 0), ... + TwoFluid = ... + struct( ... + Number = 2, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.TwoFluid.TwoFluidSolver, ... + MixtureCase = 1), ... + ThreeField = ... + struct( ... + Number = 3, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.ThreeField.ThreeFieldSolver, ... + MixtureCase = 1), ... + FourField = ... + struct( ... + Number = 4, ... + ModelID = "TUTORIAL1", ... + OptionsID = "DEFAULT", ... + GeometryID = "TUTORIAL1", ... + BoundaryCondition = "tutorial1", ... + Constructor = @Solvers.FourField.FourFieldSolver, ... + MixtureCase = 1) ... + ); + + end + methods (TestClassSetup) - function constructAndSolveTestCases(testCase) - % CONSTRUCTANDSOLVETESTCASES + function constructAndSolveTestCase(testCase, case_option) + % CONSTRUCTANDSOLVETESTCASE % % Shared setup executed once before the individual test methods. % @@ -55,7 +108,7 @@ function constructAndSolveTestCases(testCase) % 1. Determines the repository paths. % 2. Adds the OpenSTREAM root to the MATLAB path. % 3. Recreates the output directory. - % 4. Defines the solver cases. + % 4. Defines the solver case. % 5. Constructs and solves every solver object. % % The solved objects are retained in testCase.solvers and @@ -80,7 +133,7 @@ function constructAndSolveTestCases(testCase) testCase.inputFolder = fullfile(testCase.openstreamRoot,'inputs'); % Define a dedicated output folder for this test class. - testCase.outputFolder = fullfile(testCase.testFolder,'outputs'); + testCase.outputFolder = fullfile(testCase.testFolder,'outputs',string(case_option.Number)); % Define the approved-reference folder. testCase.referenceFolder = fullfile(testCase.testFolder,'references'); @@ -111,74 +164,23 @@ function constructAndSolveTestCases(testCase) % Import the OpenSTREAM packages. import Inputs.* - import Solvers.* - import Solvers.Mixture.* - import Solvers.TwoFluid.* - import Solvers.ThreeField.* - import Solvers.FourField.* - - % Define the test cases. - % - % Each structure contains: - % Number Numerical identifier used by the test class. - % ModelID Physical-model input-set identifier. - % OptionsID Numerical-option input-set identifier. - % GeometryID Geometry input-set identifier. - % BoundaryCondition Boundary-condition filename without the .inp extension. - % Constructor Function handle to the corresponding solver constructor. - % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. - - cases = [ - struct( ... - Number = 1, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @MixtureSolver, ... - MixtureCase = 0); - - struct( ... - Number = 2, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @TwoFluidSolver, ... - MixtureCase = 1); - - struct( ... - Number = 3, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @ThreeFieldSolver, ... - MixtureCase = 1); - - struct( ... - Number = 4, ... - ModelID = "TUTORIAL1", ... - OptionsID = "DEFAULT", ... - GeometryID = "TUTORIAL1", ... - BoundaryCondition = "tutorial1", ... - Constructor = @FourFieldSolver, ... - MixtureCase = 1) - ]; - - % Preallocate the shared test properties. - - numberOfCases = numel(cases); - testCase.testNumber = zeros(1,numberOfCases); - testCase.inputSets = cell(1,numberOfCases); - testCase.solvers = cell(1,numberOfCases); - - % Construct and solve every test case. - - for caseIndex = 1:numberOfCases + % import Solvers.* + % import Solvers.Mixture.* + % import Solvers.TwoFluid.* + % import Solvers.ThreeField.* + % import Solvers.FourField.* + + % % Preallocate the shared test properties. + % + % numberOfCases = numel(cases); + % testCase.testNumber = zeros(1,numberOfCases); + % testCase.inputSets = cell(1,numberOfCases); + % testCase.solvers = cell(1,numberOfCases); + + % Construct and solve test case. % Store the test identifier. - testCase.testNumber(caseIndex) = cases(caseIndex).Number; + testCase.testNumber = case_option.Number; % Preserve the current MATLAB warning configuration. warningState = warning; @@ -194,35 +196,37 @@ function constructAndSolveTestCases(testCase) % Construct the input set. inputSet = InputSet( ... modelFilePath = fullfile(testCase.inputFolder,'models.inp'), ... - modelID = cases(caseIndex).ModelID, ... + modelID = case_option.ModelID, ... optionsFilePath = fullfile(testCase.inputFolder,'options.inp'), ... - optionsID = cases(caseIndex).OptionsID, ... + optionsID = case_option.OptionsID, ... geometryFilePath = fullfile(testCase.inputFolder,'geom.inp'), ... - geometryID = cases(caseIndex).GeometryID, ... - bcFilePath = fullfile(testCase.inputFolder,cases(caseIndex).BoundaryCondition + ".inp"), ... + geometryID = case_option.GeometryID, ... + bcFilePath = fullfile(testCase.inputFolder,case_option.BoundaryCondition + ".inp"), ... sessionParentDir = testCase.outputFolder, ... - overwriteSessionFiles = true, ... + overwriteSessionFiles = false, ... LOGMODE = 'BOTH'); - testCase.inputSets{caseIndex} = inputSet; + testCase.inputSet = inputSet; % Clearing the cleanup object restores the original warning % configuration immediately after successful construction. clear warningCleanup % Construct the solver object. - if cases(caseIndex).MixtureCase == 0 - % The mixture solver does not require an existing mixture solution. - testCase.solvers{caseIndex} = cases(caseIndex).Constructor(testCase.inputSets{caseIndex}); - else - % Retrieve the solved mixture object used to initialize the advanced solver. - mixtureSolver = testCase.solvers{cases(caseIndex).MixtureCase}; - % Construct the advanced solver using the common mixture solution. - testCase.solvers{caseIndex} = cases(caseIndex).Constructor(testCase.inputSets{caseIndex},mixtureSolver); - end + % if case_option.MixtureCase == 0 + % % The mixture solver does not require an existing mixture solution. + % testCase.solver= case_option.Constructor(testCase.inputSet); + % else + % % Retrieve the solved mixture object used to initialize the advanced solver. + % mixtureSolver = testCase.solver{case_option.MixtureCase}; + % % Construct the advanced solver using the common mixture solution. + % testCase.solver = case_option.Constructor(testCase.inputSet,mixtureSolver); + % end + + testCase.solver= case_option.Constructor(testCase.inputSet); % Solve the flow fields. - testCase.solvers{caseIndex}.solve(); - end + testCase.solver.solve(); + end @@ -233,24 +237,20 @@ function constructAndSolveTestCases(testCase) function solverStates(testCase) % SOLVERSTATES % - % Verify that every solver reaches the expected converged state. - - for caseIndex = 1:numel(testCase.solvers) - - % Retrieve the calculated solver object. - results = testCase.solvers{caseIndex}; + % Verify that the solver reaches the expected converged state. - % Obtain the short solver-class name for the test message. - solverName = testCase.getSolverName(results); + % Retrieve the calculated solver object. + result = testCase.solver; - % Verify the expected solver state. - testCase.verifyEqual( ... - results.STATE, ... - Solvers.SolverState.INITIALSTEPCONVERGED, ... - solverName + ... - ": The solver did not reach the expected converged state."); + % Obtain the short solver-class name for the test message. + solverName = testCase.getSolverName(result); - end + % Verify the expected solver state. + testCase.verifyEqual( ... + result.STATE, ... + Solvers.SolverState.INITIALSTEPCONVERGED, ... + solverName + ... + ": The solver did not reach the expected converged state."); end @@ -260,16 +260,20 @@ function mixtureBoundaryConditions(testCase) % Verify that the first stored physical state satisfies the prescribed % inlet boundary conditions without modification. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; - + % Assume the mixture-solver case. + % testCase.assumeEqual( ... + % class(testCase.solver), ... + % 'Solvers.Mixture.MixtureSolver', ... + % "this test is only for the MixtureSolver case.") + + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end + % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -280,7 +284,7 @@ function mixtureBoundaryConditions(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSets{mixtureCaseIndex}; + suppliedInputSet = testCase.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -317,15 +321,13 @@ function mixtureEnergyBalance(testCase) % Verify that the first stored physical state satisfies the prescribed % steady-state energy balance. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -337,7 +339,7 @@ function mixtureEnergyBalance(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSets{mixtureCaseIndex}; + suppliedInputSet = testCase.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -370,15 +372,13 @@ function mixtureInitializationTransfer(testCase) % initialization state are transferred unchanged to the first stored % physical state. - % Locate the mixture-solver case. - mixtureCaseIndex = find(testCase.testNumber == 1,1); - - testCase.assertNotEmpty( ... - mixtureCaseIndex, ... - "The mixture-solver test case was not found."); - - % Retrieve the calculated mixture solver. - results = testCase.solvers{mixtureCaseIndex}; + if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") + % Retrieve the calculated mixture solver. + results = testCase.solver.mixSolver; + else + % Retrieve the calculated mixture solver. + results = testCase.solver; + end % Obtain the short solver-class name for test messages. solverName = testCase.getSolverName(results); @@ -461,10 +461,10 @@ function saveLoadRoundTrip(testCase) % % The solver is stored using the variable name "solver". - for solverCell = testCase.solvers + for solverCell = testCase.solver % Retrieve the original solver object. - original = solverCell{1}; + original = testCase.solver; % Obtain the short solver-class name used in the generated MAT-file name. solverName = testCase.getSolverName(original); @@ -668,128 +668,124 @@ function compareFieldProperty( ... % test. numberOfVerifiedProperties = 0; - for caseIndex = 1:numel(testCase.solvers) - - % Retrieve the calculated solver object. - results = testCase.solvers{caseIndex}; - - % Load the corresponding approved reference solver. - references = testCase.loadReference(results); - - % Obtain the short solver-class name for diagnostic messages. - solverName = testCase.getSolverName(results); - - % Count the applicable field quantities verified for this solver. - numberOfVerifiedFields = 0; - - for fieldPath = fieldPaths - - % Retrieve the calculated field object at the first stored - % physical state. - [resultField,resultFieldExists] = ... - testCase.getFieldByPath( ... - results, ... - fieldPath); - - % Retrieve the corresponding reference field object. - [referenceField,referenceFieldExists] = ... - testCase.getFieldByPath( ... - references, ... - fieldPath); - - % Verify that the calculated and reference solver structures - % expose the same field paths. - testCase.assertEqual( ... - resultFieldExists, ... - referenceFieldExists, ... - solverName + ... - ": Field path '" + fieldPath + ... - "' is not consistently available."); - - % Skip field paths that do not apply to the selected solver. - if ~resultFieldExists - continue - end - - % Retrieve the requested calculated quantity. The quantity may - % be implemented as a property or as a zero-input method. - [resultValue,resultValueExists] = ... - testCase.getFieldValue( ... - resultField, ... - propertyName); - - % Retrieve the corresponding reference quantity. - [referenceValue,referenceValueExists] = ... - testCase.getFieldValue( ... - referenceField, ... - propertyName); - - % Verify that the calculated and reference field interfaces - % expose the requested quantity consistently. - testCase.assertEqual( ... - resultValueExists, ... - referenceValueExists, ... - solverName + ... - ": Quantity '" + propertyName + ... - "' is not consistently available in field '" + ... - fieldPath + "'."); - - % Skip fields that do not expose the requested quantity. - if ~resultValueExists - continue - end - - % Verify that the calculated quantity is not empty. - testCase.assertNotEmpty( ... - resultValue, ... - solverName + ... - ": Field '" + fieldPath + ... - "' has an empty " + ... - propertyDescription + " array."); - - % Verify that the reference quantity is not empty. - testCase.assertNotEmpty( ... - referenceValue, ... - solverName + ... - ": Reference field '" + fieldPath + ... - "' has an empty " + ... - propertyDescription + " array."); - - % Compare the calculated and approved reference values. - diagnostic = sprintf( ... - '%s: %s %s mismatch using RelTol = %.1e.', ... - char(solverName), ... - char(fieldPath), ... - char(propertyDescription), ... - relativeTolerance); - - testCase.verifyEqual( ... - resultValue, ... - referenceValue, ... - diagnostic, ... - RelTol = relativeTolerance); - - % Record the completed comparison. - numberOfVerifiedFields = ... - numberOfVerifiedFields + 1; - - numberOfVerifiedProperties = ... - numberOfVerifiedProperties + 1; + % Retrieve the calculated solver object. + results = testCase.solver; + + % Load the corresponding approved reference solver. + references = testCase.loadReference(results); + + % Obtain the short solver-class name for diagnostic messages. + solverName = testCase.getSolverName(results); + + % Count the applicable field quantities verified for this solver. + numberOfVerifiedFields = 0; + + for fieldPath = fieldPaths + + % Retrieve the calculated field object at the first stored + % physical state. + [resultField,resultFieldExists] = ... + testCase.getFieldByPath( ... + results, ... + fieldPath); + + % Retrieve the corresponding reference field object. + [referenceField,referenceFieldExists] = ... + testCase.getFieldByPath( ... + references, ... + fieldPath); + + % Verify that the calculated and reference solver structures + % expose the same field paths. + testCase.assertEqual( ... + resultFieldExists, ... + referenceFieldExists, ... + solverName + ... + ": Field path '" + fieldPath + ... + "' is not consistently available."); + % Skip field paths that do not apply to the selected solver. + if ~resultFieldExists + continue end - % Ensure that at least one applicable field quantity was verified - % for the selected solver. - testCase.verifyGreaterThan( ... - numberOfVerifiedFields, ... - 0, ... + % Retrieve the requested calculated quantity. The quantity may + % be implemented as a property or as a zero-input method. + [resultValue,resultValueExists] = ... + testCase.getFieldValue( ... + resultField, ... + propertyName); + + % Retrieve the corresponding reference quantity. + [referenceValue,referenceValueExists] = ... + testCase.getFieldValue( ... + referenceField, ... + propertyName); + + % Verify that the calculated and reference field interfaces + % expose the requested quantity consistently. + testCase.assertEqual( ... + resultValueExists, ... + referenceValueExists, ... solverName + ... - ": No applicable " + ... - propertyDescription + ... - " field was found."); + ": Quantity '" + propertyName + ... + "' is not consistently available in field '" + ... + fieldPath + "'."); + + % Skip fields that do not expose the requested quantity. + if ~resultValueExists + continue + end + + % Verify that the calculated quantity is not empty. + testCase.assertNotEmpty( ... + resultValue, ... + solverName + ... + ": Field '" + fieldPath + ... + "' has an empty " + ... + propertyDescription + " array."); + + % Verify that the reference quantity is not empty. + testCase.assertNotEmpty( ... + referenceValue, ... + solverName + ... + ": Reference field '" + fieldPath + ... + "' has an empty " + ... + propertyDescription + " array."); + + % Compare the calculated and approved reference values. + diagnostic = sprintf( ... + '%s: %s %s mismatch using RelTol = %.1e.', ... + char(solverName), ... + char(fieldPath), ... + char(propertyDescription), ... + relativeTolerance); + + testCase.verifyEqual( ... + resultValue, ... + referenceValue, ... + diagnostic, ... + RelTol = relativeTolerance); + + % Record the completed comparison. + numberOfVerifiedFields = ... + numberOfVerifiedFields + 1; + + numberOfVerifiedProperties = ... + numberOfVerifiedProperties + 1; end + % Ensure that at least one applicable field quantity was verified + % for the selected solver. + testCase.verifyGreaterThan( ... + numberOfVerifiedFields, ... + 0, ... + solverName + ... + ": No applicable " + ... + propertyDescription + ... + " field was found."); + % Prevent the complete test from passing without comparing any values. testCase.verifyGreaterThan( ... numberOfVerifiedProperties, ... From 91779ef8c5a5834b017753752dfd1a014f8f1b40 Mon Sep 17 00:00:00 2001 From: lecorrejm Date: Sun, 13 Sep 2026 13:30:25 +0200 Subject: [PATCH 5/7] refactor(tests): Cleanup SolverIntegrationTests and add descriptive folder name --- tests/integration/SolverIntegrationTest.m | 172 +++++++++------------- 1 file changed, 70 insertions(+), 102 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index 519abc3..3e1576a 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -19,8 +19,11 @@ % retaining their class and solver state. % % The tests use the TUTORIAL1 model, numerical-option, geometry, and - % boundary-condition input sets. The two-fluid, three-field, and - % four-field solvers reuse the converged mixture-solver solution. + % boundary-condition input sets. + % + % Each solver framework is tested in an independent class-setup + % Parameterization. The two-fluid, three-field, and four-field + % constructors create and solve their required mixture solution. % % Approved reference solver objects are expected in: % tests/integration/references// @@ -53,52 +56,51 @@ % GeometryID Geometry input-set identifier. % BoundaryCondition Boundary-condition filename without the .inp extension. % Constructor Function handle to the corresponding solver constructor. - % MixtureCase Index of the mixture solver used for initialization. A value of zero indicates that no existing mixture solver is supplied. - case_option = struct( ... + caseOption = struct( ... Mixture = ... struct( ... + Name = "Mixture", ... Number = 1, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.Mixture.MixtureSolver, ... - MixtureCase = 0), ... + Constructor = @Solvers.Mixture.MixtureSolver), ... TwoFluid = ... struct( ... + Name = "TwoFluid", ... Number = 2, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.TwoFluid.TwoFluidSolver, ... - MixtureCase = 1), ... + Constructor = @Solvers.TwoFluid.TwoFluidSolver), ... ThreeField = ... struct( ... + Name = "ThreeField", ... Number = 3, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.ThreeField.ThreeFieldSolver, ... - MixtureCase = 1), ... + Constructor = @Solvers.ThreeField.ThreeFieldSolver), ... FourField = ... struct( ... + Name = "FourField", ... Number = 4, ... ModelID = "TUTORIAL1", ... OptionsID = "DEFAULT", ... GeometryID = "TUTORIAL1", ... BoundaryCondition = "tutorial1", ... - Constructor = @Solvers.FourField.FourFieldSolver, ... - MixtureCase = 1) ... + Constructor = @Solvers.FourField.FourFieldSolver) ... ); end methods (TestClassSetup) - function constructAndSolveTestCase(testCase, case_option) + function constructAndSolveTestCase(testCase, caseOption) % CONSTRUCTANDSOLVETESTCASE % % Shared setup executed once before the individual test methods. @@ -109,10 +111,10 @@ function constructAndSolveTestCase(testCase, case_option) % 2. Adds the OpenSTREAM root to the MATLAB path. % 3. Recreates the output directory. % 4. Defines the solver case. - % 5. Constructs and solves every solver object. + % 5. Constructs and solves the selected solver object. % - % The solved objects are retained in testCase.solvers and - % reused by all test methods. + % The solved object is retained in testCase.solver and reused by all + % test methods for the current class-setup parameterization. % Determine the test and repository paths. @@ -133,7 +135,7 @@ function constructAndSolveTestCase(testCase, case_option) testCase.inputFolder = fullfile(testCase.openstreamRoot,'inputs'); % Define a dedicated output folder for this test class. - testCase.outputFolder = fullfile(testCase.testFolder,'outputs',string(case_option.Number)); + testCase.outputFolder = fullfile(testCase.testFolder,'outputs',caseOption.Name); % Define the approved-reference folder. testCase.referenceFolder = fullfile(testCase.testFolder,'references'); @@ -162,25 +164,13 @@ function constructAndSolveTestCase(testCase, case_option) % Create a clean output directory for this test run. mkdir(testCase.outputFolder); - % Import the OpenSTREAM packages. + % Import the OpenSTREAM Inputs package. import Inputs.* - % import Solvers.* - % import Solvers.Mixture.* - % import Solvers.TwoFluid.* - % import Solvers.ThreeField.* - % import Solvers.FourField.* - - % % Preallocate the shared test properties. - % - % numberOfCases = numel(cases); - % testCase.testNumber = zeros(1,numberOfCases); - % testCase.inputSets = cell(1,numberOfCases); - % testCase.solvers = cell(1,numberOfCases); % Construct and solve test case. % Store the test identifier. - testCase.testNumber = case_option.Number; + testCase.testNumber = caseOption.Number; % Preserve the current MATLAB warning configuration. warningState = warning; @@ -196,12 +186,12 @@ function constructAndSolveTestCase(testCase, case_option) % Construct the input set. inputSet = InputSet( ... modelFilePath = fullfile(testCase.inputFolder,'models.inp'), ... - modelID = case_option.ModelID, ... + modelID = caseOption.ModelID, ... optionsFilePath = fullfile(testCase.inputFolder,'options.inp'), ... - optionsID = case_option.OptionsID, ... + optionsID = caseOption.OptionsID, ... geometryFilePath = fullfile(testCase.inputFolder,'geom.inp'), ... - geometryID = case_option.GeometryID, ... - bcFilePath = fullfile(testCase.inputFolder,case_option.BoundaryCondition + ".inp"), ... + geometryID = caseOption.GeometryID, ... + bcFilePath = fullfile(testCase.inputFolder,caseOption.BoundaryCondition + ".inp"), ... sessionParentDir = testCase.outputFolder, ... overwriteSessionFiles = false, ... LOGMODE = 'BOTH'); @@ -212,17 +202,7 @@ function constructAndSolveTestCase(testCase, case_option) clear warningCleanup % Construct the solver object. - % if case_option.MixtureCase == 0 - % % The mixture solver does not require an existing mixture solution. - % testCase.solver= case_option.Constructor(testCase.inputSet); - % else - % % Retrieve the solved mixture object used to initialize the advanced solver. - % mixtureSolver = testCase.solver{case_option.MixtureCase}; - % % Construct the advanced solver using the common mixture solution. - % testCase.solver = case_option.Constructor(testCase.inputSet,mixtureSolver); - % end - - testCase.solver= case_option.Constructor(testCase.inputSet); + testCase.solver= caseOption.Constructor(testCase.inputSet); % Solve the flow fields. testCase.solver.solve(); @@ -284,7 +264,7 @@ function mixtureBoundaryConditions(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSet; + suppliedInputSet = results.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -339,7 +319,7 @@ function mixtureEnergyBalance(testCase) ": No stored physical solution is available."); % Retrieve the input set independently retained by the test class. - suppliedInputSet = testCase.inputSet; + suppliedInputSet = results.inputSet; % Retrieve the initial boundary-condition state. boundaryConditions = suppliedInputSet.bc(1); @@ -452,71 +432,59 @@ function fieldEnthalpies(testCase) function saveLoadRoundTrip(testCase) % SAVELOADROUNDTRIP % - % Verify that each solver object: - % - % 1. Can be saved to a MAT-file. - % 2. Can be loaded from that MAT-file. - % 3. Retains the solver class. - % 4. Retains the solver state. - % - % The solver is stored using the variable name "solver". - - for solverCell = testCase.solver - - % Retrieve the original solver object. - original = testCase.solver; - - % Obtain the short solver-class name used in the generated MAT-file name. - solverName = testCase.getSolverName(original); + % Verify that the solver object can be saved and reloaded while + % retaining its class and solver state. - % Save the solver object. + % Retrieve the original solver object. + original = testCase.solver; - original.save( ... - 'name', 'solver', ... - 'showpath', false); + % Obtain the short solver-class name used in the generated MAT-file name. + solverName = testCase.getSolverName(original); - % Construct the expected saved-file path. - fileName = fullfile( ... - original.inputSet.session.parentDir, ... - original.inputSet.session.dirName, ... - original.inputSet.session.name + ... - "_" + solverName + ".mat"); + % Save the solver object. + original.save( ... + 'name', 'solver', ... + 'showpath', false); - % Confirm that the MAT-file was created. - testCase.assertTrue( ... - isfile(fileName), ... - solverName + ... - ": Saved solver file was not created."); - - % Reload the solver object. + % Construct the expected saved-file path. + fileName = fullfile( ... + original.inputSet.session.parentDir, ... + original.inputSet.session.dirName, ... + original.inputSet.session.name + ... + "_" + solverName + ".mat"); - savedData = load(fileName); + % Confirm that the MAT-file was created. + testCase.assertTrue( ... + isfile(fileName), ... + solverName + ... + ": Saved solver file was not created."); - % Confirm that the selected variable name was stored in the MAT-file. - testCase.assertTrue( ... - isfield(savedData,'solver'), ... - solverName + ... - ": Saved variable 'solver' is missing."); + % Reload the solver object. + savedData = load(fileName); - restored = savedData.solver; + % Confirm that the selected variable name was stored in the MAT-file. + testCase.assertTrue( ... + isfield(savedData,'solver'), ... + solverName + ... + ": Saved variable 'solver' is missing."); - % Verify the restored object. + restored = savedData.solver; - % Verify that the restored object has the same solver class. - testCase.verifyEqual( ... - class(restored), ... - class(original), ... - solverName + ... - ": Restored solver class mismatch."); + % Verify the restored object. - % Verify that the solver state was preserved. - testCase.verifyEqual( ... - restored.STATE, ... - original.STATE, ... - solverName + ... - ": Restored solver state mismatch."); + % Verify that the restored object has the same solver class. + testCase.verifyEqual( ... + class(restored), ... + class(original), ... + solverName + ... + ": Restored solver class mismatch."); - end + % Verify that the solver state was preserved. + testCase.verifyEqual( ... + restored.STATE, ... + original.STATE, ... + solverName + ... + ": Restored solver state mismatch."); end From 33ee3a5219e61542a56210e365d73a2243dbb798 Mon Sep 17 00:00:00 2001 From: lecorrejm Date: Sun, 13 Sep 2026 15:18:31 +0200 Subject: [PATCH 6/7] refactor(tests): Cleanup SolverIntegrationTests --- tests/integration/SolverIntegrationTest.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/SolverIntegrationTest.m b/tests/integration/SolverIntegrationTest.m index 3e1576a..3a84e09 100644 --- a/tests/integration/SolverIntegrationTest.m +++ b/tests/integration/SolverIntegrationTest.m @@ -239,12 +239,6 @@ function mixtureBoundaryConditions(testCase) % % Verify that the first stored physical state satisfies the prescribed % inlet boundary conditions without modification. - - % Assume the mixture-solver case. - % testCase.assumeEqual( ... - % class(testCase.solver), ... - % 'Solvers.Mixture.MixtureSolver', ... - % "this test is only for the MixtureSolver case.") if ~isa(testCase.solver, "Solvers.Mixture.MixtureSolver") % Retrieve the calculated mixture solver. From a840ad6ed37ecc56a66ca33d61899c7c7d3b91e2 Mon Sep 17 00:00:00 2001 From: Jason Chan Date: Fri, 25 Sep 2026 14:25:32 -0500 Subject: [PATCH 7/7] fix: limit tests to those in the tests folder --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76bd1d7..a0a6017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,7 @@ jobs: with: output-detail: verbose source-folder: ./openstream + select-by-folder: ./openstream/tests # Upload artifacts - name: Upload MATLAB test output