From 1bb617770f9cced6f555154dc180b5a77ae9b58e Mon Sep 17 00:00:00 2001 From: Nicholas Wiltsie Date: Fri, 13 Dec 2024 10:23:37 -0800 Subject: [PATCH 1/2] Add mockito into test Docker image --- tests/Dockerfile | 2 +- tests/config/pom.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Dockerfile b/tests/Dockerfile index 98fa51c..d046586 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -19,7 +19,7 @@ ENV NXF_LAUNCHER=/.nextflow/tmp/launcher/nextflow-one_${NEXTFLOW_VERSION}/buildk RUN sed \ -i \ -e 's/"nextflow.cli.Launcher"/"groovy.ui.GroovyMain"/' \ - -e 's|"-classpath" "|"-classpath" "/bljars/junit-4.13.2.jar:/bljars/hamcrest-core-1.3.jar:/bljars/groovy-test-3.0.19.jar:/bljars/system-rules-1.19.0.jar:$BL_CLASSPATH:|' \ + -e "s|\"-classpath\" \"|\"-classpath\" \"$(find /bljars/ -not -name 'groovy-3*' -type f -printf "%p:"):|" \ ${NXF_LAUNCHER}/classpath-${NEXTFLOW_MD5} COPY validator /usr/local/validator diff --git a/tests/config/pom.xml b/tests/config/pom.xml index 91aba18..5509548 100644 --- a/tests/config/pom.xml +++ b/tests/config/pom.xml @@ -15,5 +15,10 @@ system-rules 1.19.0 + + org.mockito + mockito-core + 5.10.0 + From 6edfefed061ec41e0e802e3a699b9e1dbb04363e Mon Sep 17 00:00:00 2001 From: Nicholas Wiltsie Date: Fri, 13 Dec 2024 10:48:34 -0800 Subject: [PATCH 2/2] Add test for resource handling --- tests/ResourceTests.groovy | 230 +++++++++++++++++++++++++ tests/resource_test/config/base.config | 0 tests/suite.groovy | 3 +- 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 tests/ResourceTests.groovy create mode 100644 tests/resource_test/config/base.config diff --git a/tests/ResourceTests.groovy b/tests/ResourceTests.groovy new file mode 100644 index 0000000..59b0ba8 --- /dev/null +++ b/tests/ResourceTests.groovy @@ -0,0 +1,230 @@ +import java.nio.file.Path +import java.nio.file.Paths + +import static groovy.test.GroovyAssert.shouldFail +import groovy.json.JsonOutput +import groovy.util.ConfigObject + +import nextflow.util.ConfigHelper +import nextflow.util.MemoryUnit +import nextflow.util.SysHelper +import org.junit.Test + +import validator.bl.NextflowConfigTests + +class ResourceTests extends NextflowConfigTests { + protected Path get_projectDir() { + // Return the path to the "resource_test/" subfolder + return Paths.get( + getClass().protectionDomain.codeSource.location.path + ).getParent().resolve("resource_test") + } + + protected int override_cpus = 10 + protected MemoryUnit override_memory = 12.GB + + @Override + protected def generate_config_text(configobj) { + return """ + import nextflow.util.SysHelper + import nextflow.util.MemoryUnit + import static org.mockito.Mockito.* + import org.mockito.MockedStatic + + // Mock out the SysHelper::getAvailCpus() and + // SysHelper::getAvailMemory() methods + + try (MockedStatic dummyhelper = mockStatic( + SysHelper.class, + CALLS_REAL_METHODS)) { + dummyhelper + .when(SysHelper::getAvailCpus) + .thenReturn(${override_cpus}); + dummyhelper + .when(SysHelper::getAvailMemory) + .thenReturn(new MemoryUnit(${override_memory.getBytes()})); + + includeConfig "\${projectDir}/../../config/resource_handler/resource_handler.config" + + ${ConfigHelper.toCanonicalString(configobj)} + resource_handler.handle_resources(params.resource_file) + } + """ + } + + protected Map get_baseline_resource_allocations() { + // These are modified from the resource allocation README + return [ + default: [ + process1: [ + cpus: [ min: 1, fraction: 0.51, max: 100 ], + memory: [ min: "1 MB", fraction: 0.5, max: "100 GB" ] + ], + process2: [ + cpus: [ min: 1, fraction: 0.75, max: 100 ], + memory: [ min: "1 MB", fraction: 0.25, max: "100 GB" ] + ], + process3: [ + cpus: [ min: 1, fraction: 0.75, max: 2 ], + memory: [ min: "1 MB", fraction: 0.5, max: "12 MB" ] + ] + ], + custom_profile: [ + process1: [ + cpus: [ min: 12, fraction: 0.25, max: 100 ], + memory: [ min: "5 GB", fraction: 1.0, max: "100 GB" ] + ], + process2: [ + cpus: [ min: 12, fraction: 0.25, max: 20], + memory: [ min: "230 MB", fraction: 0.25, max: "250 MB" ] + ], + process3: [ + cpus: [ min: 7, fraction: 0.75, max: 1000 ], + memory: [ min: "12 GB", fraction: 0.6, max: "120 GB" ] + ] + ] + ] + } + + protected String write_resource_json(Map resources) { + File tempfile = testFolder.newFile("resources.json") + tempfile.write(JsonOutput.prettyPrint(JsonOutput.toJson(resources))) + return tempfile.toString() + } + + protected def set_common_parameters(Map resources) { + File tempfile = testFolder.newFile("resources.json") + tempfile.write(JsonOutput.prettyPrint(JsonOutput.toJson(resources))) + + inconfig.params.resource_file = tempfile.toString() + expected.params.resource_file = tempfile.toString() + + // These parameters are scraped from the current system + expected.params.min_cpus = 1 + expected.params.max_cpus = override_cpus + + expected.params.min_memory = 1.MB + expected.params.max_memory = override_memory + + expected.params.min_time = 1.s + expected.params.max_time = 1000.d + + // Re-implement the logic from the resource handler to predict the values + expected.params.base_allocations = [:] + + def profile = resources.get( + inconfig.params.containsKey("resource_allocation_profile_tag") + ? inconfig.params.resource_allocation_profile_tag + : "default" + ) + + profile.each { process_name, process_info -> + def allocations = [:] + + allocations.cpus = Math.min( + Math.min( + Math.max( + (override_cpus * process_info.cpus.fraction).asType(Integer), + process_info.cpus.min + ), + process_info.cpus.max + ), + override_cpus + ) + + allocations.memory = MemoryUnit.of(Math.min( + Math.min( + Math.max( + (override_memory.getBytes() * process_info.memory.fraction).asType(long), + MemoryUnit.of(process_info.memory.min).getBytes() + ), + MemoryUnit.of(process_info.memory.max).getBytes() + ), + override_memory.getBytes() + )) + + expected.params.base_allocations[process_name] = allocations + } + + expected.params.retry_information = [:] + } + + // A helper method to compare that the values of any common keys between + // the two maps are equal. + def compare_common_keys(Map left, Map right) { + left.keySet().intersect(right.keySet()).each { key -> + if (left[key] instanceof Map) { + assert right[key] instanceof Map + compare_common_keys(left[key], right[key]) + } else { + assert left[key] == right[key] + } + } + } + + @Test + void test_defaults() { + def resources = get_baseline_resource_allocations() + set_common_parameters(resources) + + // Sanity check - should get 50% of the memory in the default profile + assert expected.params.base_allocations.process1.memory == 0.5 * override_memory + + compare() + } + + @Test + void test_custom_profile() { + def resources = get_baseline_resource_allocations() + inconfig.params.resource_allocation_profile_tag = "custom_profile" + expected.params.resource_allocation_profile_tag = "custom_profile" + set_common_parameters(resources) + + // Sanity check - should get 100% of the memory in the custom profile + assert expected.params.base_allocations.process1.memory == override_memory + + compare() + } + + @Test + void test_modified_parameters() { + def resources = get_baseline_resource_allocations() + + def meta_expected = new ConfigObject() + + // Tweak the system on which this is being evaluated + override_cpus = 1000 + override_memory *= 2 + + // Sanity check - process1 should get 50% of the memory + meta_expected.process1.memory = 0.5 * override_memory + + // If we pin the min and max CPUs, that should determine exactly the number we get + resources.default.process1.cpus.min = 34 + resources.default.process1.cpus.max = 34 + meta_expected.process1.cpus = 34 + + // If the max CPUs are limiting, that determines the CPU limit + resources.default.process2.cpus.min = 1 + resources.default.process2.cpus.max = 72 + meta_expected.process2.cpus = 72 + + set_common_parameters(resources) + + compare_common_keys(expected.params.base_allocations, meta_expected) + + // Sanity-check the compare_common_keys function + // Extraneous keys don't cause problems + meta_expected.process7 = [:] + meta_expected.process2.fakekey = 12 + compare_common_keys(expected.params.base_allocations, meta_expected) + + // Mismatched keys _must_ cause problems + meta_expected.process2.cpus = 71 + shouldFail { + compare_common_keys(expected.params.base_allocations, meta_expected) + } + + compare() + } +} diff --git a/tests/resource_test/config/base.config b/tests/resource_test/config/base.config new file mode 100644 index 0000000..e69de29 diff --git a/tests/suite.groovy b/tests/suite.groovy index bf7244a..f085fc8 100644 --- a/tests/suite.groovy +++ b/tests/suite.groovy @@ -5,7 +5,8 @@ result = JUnitCore.runClasses \ ExampleTests, \ SetEnvTests, \ AlignMethodsTests, \ - BamParserTests + BamParserTests, \ + ResourceTests String message = "Ran: " + result.getRunCount() + ", Ignored: " + result.getIgnoreCount() + ", Failed: " + result.getFailureCount() if (result.wasSuccessful()) {