From aeaef79d2557c66f9dddb989147a539a2d5b439e Mon Sep 17 00:00:00 2001 From: Brian Gunnarson Date: Wed, 3 May 2023 17:20:14 -0700 Subject: [PATCH 01/48] fix default worker bug with all steps --- CHANGELOG.md | 7 +++ merlin/spec/specification.py | 7 ++- tests/integration/test_definitions.py | 10 ++++ .../test_specs/default_worker_test.yaml | 56 +++++++++++++++++++ .../test_specs/no_default_worker_test.yaml | 56 +++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 tests/integration/test_specs/default_worker_test.yaml create mode 100644 tests/integration/test_specs/no_default_worker_test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 42811b601..e5ba817ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +- A bug where assigning a worker all steps also assigned steps to the default worker + +### Added +- Tests to make sure the default worker is being assigned properly + ## [1.10.0] ### Fixed - Pip wheel wasn't including .sh files for merlin examples diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 6fb524f5b..9f3a6c16f 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -385,8 +385,11 @@ def process_spec_defaults(self): MerlinSpec.fill_missing_defaults(worker_settings, defaults.WORKER) worker_steps.extend(worker_settings["steps"]) - # Figure out which steps still need workers - steps_that_need_workers = list(set(all_workflow_steps) - set(worker_steps)) + if "all" in worker_steps: + steps_that_need_workers = [] + else: + # Figure out which steps still need workers + steps_that_need_workers = list(set(all_workflow_steps) - set(worker_steps)) # If there are still steps remaining that haven't been assigned a worker yet, # assign the remaining steps to the default worker. If all the steps still need workers diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index 6fd8e3ce9..2b990ea40 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -283,6 +283,16 @@ def define_tests(): # pylint: disable=R0914,R0915 "conditions": [HasReturnCode(), HasRegex("custom_verify_queue")], "run type": "local", }, + "default_worker assigned": { + "cmds": f"{workers} {test_specs}/default_worker_test.yaml --echo", + "conditions": [HasReturnCode(), HasRegex(r"default_worker.*-Q '\[merlin\]_step_4_queue'")], + "run type": "local", + }, + "no default_worker assigned": { + "cmds": f"{workers} {test_specs}/no_default_worker_test.yaml --echo", + "conditions": [HasReturnCode(), HasRegex(r"default_worker", negate=True)], + "run type": "local", + }, } wf_format_tests = { "local minimum_format": { diff --git a/tests/integration/test_specs/default_worker_test.yaml b/tests/integration/test_specs/default_worker_test.yaml new file mode 100644 index 000000000..974921593 --- /dev/null +++ b/tests/integration/test_specs/default_worker_test.yaml @@ -0,0 +1,56 @@ +description: + name: multiple_workers + description: a very simple merlin workflow with multiple workers + +global.parameters: + GREET: + values : ["hello","hola"] + label : GREET.%% + WORLD: + values : ["world","mundo"] + label : WORLD.%% + +study: + - name: step_1 + description: say hello + run: + cmd: | + echo "$(GREET), $(WORLD)!" + task_queue: hello_queue + + - name: step_2 + description: step 2 + run: + cmd: | + echo "step_2" + depends: [step_1_*] + task_queue: echo_queue + + - name: step_3 + description: stop workers + run: + cmd: | + echo "stop workers" + depends: [step_2] + task_queue: other_queue + + - name: step_4 + description: another step + run: + cmd: | + echo "another step" + depends: [step_3] + task_queue: step_4_queue + +merlin: + resources: + workers: + step_1_merlin_test_worker: + args: -l INFO + steps: [step_1] + step_2_merlin_test_worker: + args: -l INFO + steps: [step_2] + other_merlin_test_worker: + args: -l INFO + steps: [step_3] diff --git a/tests/integration/test_specs/no_default_worker_test.yaml b/tests/integration/test_specs/no_default_worker_test.yaml new file mode 100644 index 000000000..b2f15d2ee --- /dev/null +++ b/tests/integration/test_specs/no_default_worker_test.yaml @@ -0,0 +1,56 @@ +description: + name: multiple_workers + description: a very simple merlin workflow with multiple workers + +global.parameters: + GREET: + values : ["hello","hola"] + label : GREET.%% + WORLD: + values : ["world","mundo"] + label : WORLD.%% + +study: + - name: step_1 + description: say hello + run: + cmd: | + echo "$(GREET), $(WORLD)!" + task_queue: hello_queue + + - name: step_2 + description: step 2 + run: + cmd: | + echo "step_2" + depends: [step_1_*] + task_queue: echo_queue + + - name: step_3 + description: stop workers + run: + cmd: | + echo "stop workers" + depends: [step_2] + task_queue: other_queue + + - name: step_4 + description: another step + run: + cmd: | + echo "another step" + depends: [step_3] + task_queue: step_4_queue + +merlin: + resources: + workers: + step_1_merlin_test_worker: + args: -l INFO + steps: [step_1] + step_2_merlin_test_worker: + args: -l INFO + steps: [step_2] + other_merlin_test_worker: + args: -l INFO + steps: [all] From f7508dd9dcf51dc2ada4ba8f64835466975f360f Mon Sep 17 00:00:00 2001 From: Brian Gunnarson Date: Thu, 4 May 2023 14:08:56 -0700 Subject: [PATCH 02/48] version bump and requirements fix --- CHANGELOG.md | 5 ++++- Makefile | 2 +- merlin/__init__.py | 4 ++-- merlin/ascii_art.py | 2 +- merlin/celery.py | 2 +- merlin/common/__init__.py | 2 +- merlin/common/abstracts/__init__.py | 2 +- merlin/common/abstracts/enums/__init__.py | 2 +- merlin/common/openfilelist.py | 2 +- merlin/common/opennpylib.py | 2 +- merlin/common/sample_index.py | 2 +- merlin/common/sample_index_factory.py | 2 +- merlin/common/security/__init__.py | 2 +- merlin/common/security/encrypt.py | 2 +- merlin/common/security/encrypt_backend_traffic.py | 2 +- merlin/common/tasks.py | 2 +- merlin/common/util_sampling.py | 2 +- merlin/config/__init__.py | 2 +- merlin/config/broker.py | 2 +- merlin/config/celeryconfig.py | 2 +- merlin/config/configfile.py | 2 +- merlin/config/results_backend.py | 2 +- merlin/config/utils.py | 2 +- merlin/data/celery/__init__.py | 2 +- merlin/display.py | 2 +- merlin/examples/__init__.py | 2 +- merlin/examples/examples.py | 2 +- merlin/examples/generator.py | 2 +- merlin/examples/workflows/feature_demo/requirements.txt | 2 +- .../examples/workflows/remote_feature_demo/requirements.txt | 2 +- merlin/exceptions/__init__.py | 2 +- merlin/log_formatter.py | 2 +- merlin/main.py | 2 +- merlin/merlin_templates.py | 2 +- merlin/router.py | 2 +- merlin/server/__init__.py | 2 +- merlin/server/server_commands.py | 2 +- merlin/server/server_config.py | 2 +- merlin/server/server_util.py | 2 +- merlin/spec/__init__.py | 2 +- merlin/spec/all_keys.py | 2 +- merlin/spec/defaults.py | 2 +- merlin/spec/expansion.py | 2 +- merlin/spec/override.py | 2 +- merlin/spec/specification.py | 2 +- merlin/study/__init__.py | 2 +- merlin/study/batch.py | 2 +- merlin/study/celeryadapter.py | 2 +- merlin/study/dag.py | 2 +- merlin/study/script_adapter.py | 2 +- merlin/study/step.py | 2 +- merlin/study/study.py | 2 +- merlin/utils.py | 2 +- setup.py | 2 +- tests/integration/conditions.py | 2 +- tests/integration/run_tests.py | 2 +- tests/integration/test_definitions.py | 2 +- 57 files changed, 61 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ba817ef..15c9e0ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,16 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.10.1] ### Fixed - A bug where assigning a worker all steps also assigned steps to the default worker ### Added - Tests to make sure the default worker is being assigned properly +### Changed +- Requirement name in examples/workflows/remote_feature_demo/requirements.txt and examples/workflows/feature_demo/requirements.txt from sklearn to scikit-learn since sklearn is now deprecated + ## [1.10.0] ### Fixed - Pip wheel wasn't including .sh files for merlin examples diff --git a/Makefile b/Makefile index 25266ace9..0153f10d4 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/__init__.py b/merlin/__init__.py index b4df0fa52..32bf5c875 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # @@ -38,7 +38,7 @@ import sys -__version__ = "1.10.0" +__version__ = "1.10.1" VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/ascii_art.py b/merlin/ascii_art.py index 02339d429..bb804d876 100644 --- a/merlin/ascii_art.py +++ b/merlin/ascii_art.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/celery.py b/merlin/celery.py index d09262adc..072c83b58 100644 --- a/merlin/celery.py +++ b/merlin/celery.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/__init__.py b/merlin/common/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/common/__init__.py +++ b/merlin/common/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/__init__.py b/merlin/common/abstracts/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/common/abstracts/__init__.py +++ b/merlin/common/abstracts/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/enums/__init__.py b/merlin/common/abstracts/enums/__init__.py index f35a75ae3..81c8865e5 100644 --- a/merlin/common/abstracts/enums/__init__.py +++ b/merlin/common/abstracts/enums/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/openfilelist.py b/merlin/common/openfilelist.py index dd12cfa93..f51055ab5 100644 --- a/merlin/common/openfilelist.py +++ b/merlin/common/openfilelist.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/opennpylib.py b/merlin/common/opennpylib.py index ceadfa5d2..65d503564 100644 --- a/merlin/common/opennpylib.py +++ b/merlin/common/opennpylib.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index.py b/merlin/common/sample_index.py index 34ccb07b0..635dd617b 100644 --- a/merlin/common/sample_index.py +++ b/merlin/common/sample_index.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index_factory.py b/merlin/common/sample_index_factory.py index 9f05e43d9..eb24e067e 100644 --- a/merlin/common/sample_index_factory.py +++ b/merlin/common/sample_index_factory.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/__init__.py b/merlin/common/security/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/common/security/__init__.py +++ b/merlin/common/security/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt.py b/merlin/common/security/encrypt.py index 819fbc8e7..22c505df4 100644 --- a/merlin/common/security/encrypt.py +++ b/merlin/common/security/encrypt.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt_backend_traffic.py b/merlin/common/security/encrypt_backend_traffic.py index 13304a443..4d7dc176f 100644 --- a/merlin/common/security/encrypt_backend_traffic.py +++ b/merlin/common/security/encrypt_backend_traffic.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index 15c915bf3..3b41cb459 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/util_sampling.py b/merlin/common/util_sampling.py index 6d5f57cff..43dbd133a 100644 --- a/merlin/common/util_sampling.py +++ b/merlin/common/util_sampling.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/__init__.py b/merlin/config/__init__.py index 5ee7ed06e..aee1b62b0 100644 --- a/merlin/config/__init__.py +++ b/merlin/config/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/broker.py b/merlin/config/broker.py index 466bc56ee..0cf9b7c6d 100644 --- a/merlin/config/broker.py +++ b/merlin/config/broker.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/celeryconfig.py b/merlin/config/celeryconfig.py index c2cc4aab5..2740aa0a1 100644 --- a/merlin/config/celeryconfig.py +++ b/merlin/config/celeryconfig.py @@ -10,7 +10,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/configfile.py b/merlin/config/configfile.py index 38043c115..6aa38f9b2 100644 --- a/merlin/config/configfile.py +++ b/merlin/config/configfile.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index de092b7a4..691bf5a6c 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/utils.py b/merlin/config/utils.py index f4cc093cc..9778cde3c 100644 --- a/merlin/config/utils.py +++ b/merlin/config/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/data/celery/__init__.py b/merlin/data/celery/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/data/celery/__init__.py +++ b/merlin/data/celery/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/display.py b/merlin/display.py index 0263e6c7c..89d14781e 100644 --- a/merlin/display.py +++ b/merlin/display.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/__init__.py b/merlin/examples/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/examples/__init__.py +++ b/merlin/examples/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/examples.py b/merlin/examples/examples.py index 7181f055c..a66abf64a 100644 --- a/merlin/examples/examples.py +++ b/merlin/examples/examples.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index 84c38d0b2..b859ff605 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/workflows/feature_demo/requirements.txt b/merlin/examples/workflows/feature_demo/requirements.txt index 93909e8aa..e308e1895 100644 --- a/merlin/examples/workflows/feature_demo/requirements.txt +++ b/merlin/examples/workflows/feature_demo/requirements.txt @@ -1,2 +1,2 @@ -sklearn +scikit-learn merlin-spellbook diff --git a/merlin/examples/workflows/remote_feature_demo/requirements.txt b/merlin/examples/workflows/remote_feature_demo/requirements.txt index 93909e8aa..e308e1895 100644 --- a/merlin/examples/workflows/remote_feature_demo/requirements.txt +++ b/merlin/examples/workflows/remote_feature_demo/requirements.txt @@ -1,2 +1,2 @@ -sklearn +scikit-learn merlin-spellbook diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index 5273fb8e7..cfbc21e38 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/log_formatter.py b/merlin/log_formatter.py index 8dc3a83e9..580952b1d 100644 --- a/merlin/log_formatter.py +++ b/merlin/log_formatter.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/main.py b/merlin/main.py index 41dbb05e3..f49d3cc94 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/merlin_templates.py b/merlin/merlin_templates.py index 01a86b2ae..4809c7aee 100644 --- a/merlin/merlin_templates.py +++ b/merlin/merlin_templates.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/router.py b/merlin/router.py index a48f86ed4..160909ddf 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/__init__.py b/merlin/server/__init__.py index 6fae626c9..7c6148fee 100644 --- a/merlin/server/__init__.py +++ b/merlin/server/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index e0a481171..d53463c2b 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_config.py b/merlin/server/server_config.py index 445495802..14abfb0d4 100644 --- a/merlin/server/server_config.py +++ b/merlin/server/server_config.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_util.py b/merlin/server/server_util.py index 7a5da16f5..d101af2f0 100644 --- a/merlin/server/server_util.py +++ b/merlin/server/server_util.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/__init__.py b/merlin/spec/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/spec/__init__.py +++ b/merlin/spec/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/all_keys.py b/merlin/spec/all_keys.py index 858c8401e..f58731336 100644 --- a/merlin/spec/all_keys.py +++ b/merlin/spec/all_keys.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index e2dc3f651..b20ce7d09 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/expansion.py b/merlin/spec/expansion.py index a535c7bd9..38f03d6e9 100644 --- a/merlin/spec/expansion.py +++ b/merlin/spec/expansion.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/override.py b/merlin/spec/override.py index 7862993fe..0f1e71f62 100644 --- a/merlin/spec/override.py +++ b/merlin/spec/override.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 9f3a6c16f..8d36297ab 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/__init__.py b/merlin/study/__init__.py index d90050599..1e9c75683 100644 --- a/merlin/study/__init__.py +++ b/merlin/study/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/batch.py b/merlin/study/batch.py index e05342fd1..eeaead5ee 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index 7c2e42b72..4c01cfd73 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/dag.py b/merlin/study/dag.py index bdc508f87..a85a86e47 100644 --- a/merlin/study/dag.py +++ b/merlin/study/dag.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index e637c0939..2df053051 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/step.py b/merlin/study/step.py index 7302344ff..14f273dfa 100644 --- a/merlin/study/step.py +++ b/merlin/study/step.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/study.py b/merlin/study/study.py index 9ee0c131d..3a51c926e 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/utils.py b/merlin/utils.py index ed7f12b9a..7c18407e7 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/setup.py b/setup.py index efbc5a529..9693b06ba 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index 22d911e7e..db21e5429 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index c858649c8..9b0270d3b 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index 2b990ea40..093644f9f 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.0. +# This file is part of Merlin, Version: 1.10.1. # # For details, see https://github.com/LLNL/merlin. # From 98e06f6557774f9cac005d03511151f567c17f56 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Thu, 25 May 2023 13:05:39 -0700 Subject: [PATCH 03/48] Bugfix/filename-special-vars (#425) * fix file naming bug * fix filename bug with variable as study name * add tests for the file name special vars changes * modify changelog * implement Luc's suggestions * remove replace line --- CHANGELOG.md | 11 ++++++ merlin/study/study.py | 57 +++++++++++++-------------- tests/integration/conditions.py | 12 ++++-- tests/integration/test_definitions.py | 24 +++++++---- tests/unit/study/test_study.py | 50 ++++++++++++++++++++++- 5 files changed, 111 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c9e0ffa..603086aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] +### Fixed +- A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name + +### Added +- Tests for ensuring `$(MERLIN_SPEC_ORIGINAL_TEMPLATE)`, `$(MERLIN_SPEC_ARCHIVED_COPY)`, and `$(MERLIN_SPEC_EXECUTED_RUN)` are stored correctly + +### Changed +- The ProvenanceYAMLFileHasRegex condition for integration tests now saves the study name and spec file name as attributes instead of just the study name + - This lead to minor changes in 3 tests ("local override feature demo", "local pgen feature demo", and "remote feature demo") with what we pass to this specific condition + ## [1.10.1] ### Fixed - A bug where assigning a worker all steps also assigned steps to the default worker diff --git a/merlin/study/study.py b/merlin/study/study.py index 3a51c926e..5446c84a1 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -36,6 +36,7 @@ import time from contextlib import suppress from copy import deepcopy +from pathlib import Path from cached_property import cached_property from maestrowf.datastructures.core import Study @@ -83,6 +84,7 @@ def __init__( # pylint: disable=R0913 pgen_file=None, pargs=None, ): + self.filepath = filepath self.original_spec = MerlinSpec.load_specification(filepath) self.override_vars = override_vars error_override_vars(self.override_vars, self.original_spec.path) @@ -114,19 +116,8 @@ def __init__( # pylint: disable=R0913 # below will be substituted for sample values on execution "MERLIN_SAMPLE_VECTOR": " ".join([f"$({k})" for k in self.get_sample_labels(from_spec=self.original_spec)]), "MERLIN_SAMPLE_NAMES": " ".join(self.get_sample_labels(from_spec=self.original_spec)), - "MERLIN_SPEC_ORIGINAL_TEMPLATE": os.path.join( - self.info, - self.original_spec.description["name"].replace(" ", "_") + ".orig.yaml", - ), - "MERLIN_SPEC_EXECUTED_RUN": os.path.join( - self.info, - self.original_spec.description["name"].replace(" ", "_") + ".partial.yaml", - ), - "MERLIN_SPEC_ARCHIVED_COPY": os.path.join( - self.info, - self.original_spec.description["name"].replace(" ", "_") + ".expanded.yaml", - ), } + self._set_special_file_vars() self.pgen_file = pgen_file self.pargs = pargs @@ -134,12 +125,27 @@ def __init__( # pylint: disable=R0913 self.dag = None self.load_dag() - def write_original_spec(self, filename): + def _set_special_file_vars(self): + """Setter for the orig, partial, and expanded file paths of a study.""" + base_name = Path(self.filepath).stem + self.special_vars["MERLIN_SPEC_ORIGINAL_TEMPLATE"] = os.path.join( + self.info, + base_name + ".orig.yaml", + ) + self.special_vars["MERLIN_SPEC_EXECUTED_RUN"] = os.path.join( + self.info, + base_name + ".partial.yaml", + ) + self.special_vars["MERLIN_SPEC_ARCHIVED_COPY"] = os.path.join( + self.info, + base_name + ".expanded.yaml", + ) + + def write_original_spec(self): """ - Copy the original spec into merlin_info/ as '.orig.yaml'. + Copy the original spec into merlin_info/ as '.orig.yaml'. """ - spec_name = os.path.join(self.info, filename + ".orig.yaml") - shutil.copyfile(self.original_spec.path, spec_name) + shutil.copyfile(self.original_spec.path, self.special_vars["MERLIN_SPEC_ORIGINAL_TEMPLATE"]) def label_clash_error(self): """ @@ -368,10 +374,6 @@ def expanded_spec(self): return self.get_expanded_spec() result = self.get_expanded_spec() - expanded_name = result.description["name"].replace(" ", "_") + ".expanded.yaml" - - # Set expanded filepath - expanded_filepath = os.path.join(self.info, expanded_name) # expand provenance spec filename if contains_token(self.original_spec.name) or contains_shell_ref(self.original_spec.name): @@ -394,8 +396,8 @@ def expanded_spec(self): self.workspace = expanded_workspace self.info = os.path.join(self.workspace, "merlin_info") self.special_vars["MERLIN_INFO"] = self.info + self._set_special_file_vars() - expanded_filepath = os.path.join(self.info, expanded_name) new_spec_text = expand_by_line(result.dump(), MerlinStudy.get_user_vars(result)) result = MerlinSpec.load_spec_from_string(new_spec_text) result = expand_env_vars(result) @@ -412,15 +414,13 @@ def expanded_spec(self): os.path.join(self.info, os.path.basename(self.samples_file)), ) - # write expanded spec for provenance - with open(expanded_filepath, "w") as f: # pylint: disable=C0103 + # write expanded spec for provenance and set the path (necessary for testing) + with open(self.special_vars["MERLIN_SPEC_ARCHIVED_COPY"], "w") as f: # pylint: disable=C0103 f.write(result.dump()) + result.path = self.special_vars["MERLIN_SPEC_ARCHIVED_COPY"] # write original spec for provenance - result = MerlinSpec.load_spec_from_string(result.dump()) - result.path = expanded_filepath - name = result.description["name"].replace(" ", "_") - self.write_original_spec(name) + self.write_original_spec() # write partially-expanded spec for provenance partial_spec = deepcopy(self.original_spec) @@ -428,8 +428,7 @@ def expanded_spec(self): partial_spec.environment["variables"] = result.environment["variables"] if "labels" in result.environment: partial_spec.environment["labels"] = result.environment["labels"] - partial_spec_path = os.path.join(self.info, name + ".partial.yaml") - with open(partial_spec_path, "w") as f: # pylint: disable=C0103 + with open(self.special_vars["MERLIN_SPEC_EXECUTED_RUN"], "w") as f: # pylint: disable=C0103 f.write(partial_spec.dump()) LOG.info(f"Study workspace is '{self.workspace}'.") diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index db21e5429..4da8c36a1 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -249,14 +249,16 @@ class ProvenanceYAMLFileHasRegex(HasRegex): MUST contain a given regular expression. """ - def __init__(self, regex, name, output_path, provenance_type, negate=False): # pylint: disable=R0913 + def __init__(self, regex, spec_file_name, study_name, output_path, provenance_type, negate=False): # pylint: disable=R0913 """ :param `regex`: a string regex pattern - :param `name`: the name of a study + :param `spec_file_name`: the name of the spec file + :param `study_name`: the name of a study :param `output_path`: the $(OUTPUT_PATH) of a study """ super().__init__(regex, negate=negate) - self.name = name + self.spec_file_name = spec_file_name + self.study_name = study_name self.output_path = output_path provenance_types = ["orig", "partial", "expanded"] if provenance_type not in provenance_types: @@ -277,7 +279,9 @@ def glob_string(self): """ Returns a regex string for the glob library to recursively find files with. """ - return f"{self.output_path}/{self.name}" f"_[0-9]*-[0-9]*/merlin_info/{self.name}.{self.prov_type}.yaml" + return ( + f"{self.output_path}/{self.study_name}" f"_[0-9]*-[0-9]*/merlin_info/{self.spec_file_name}.{self.prov_type}.yaml" + ) def is_within(self): # pylint: disable=W0221 """ diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index 093644f9f..cf7c008de 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -435,31 +435,36 @@ def define_tests(): # pylint: disable=R0914,R0915 HasReturnCode(), ProvenanceYAMLFileHasRegex( regex=r"HELLO: \$\(SCRIPTS\)/hello_world.py", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="orig", ), ProvenanceYAMLFileHasRegex( regex=r"name: \$\(NAME\)", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="partial", ), ProvenanceYAMLFileHasRegex( regex="studies/feature_demo_", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="partial", ), ProvenanceYAMLFileHasRegex( regex="name: feature_demo", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="expanded", ), ProvenanceYAMLFileHasRegex( regex=r"\$\(NAME\)", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="expanded", negate=True, @@ -510,13 +515,15 @@ def define_tests(): # pylint: disable=R0914,R0915 "conditions": [ ProvenanceYAMLFileHasRegex( regex=r"\[0.3333333", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="expanded", ), ProvenanceYAMLFileHasRegex( regex=r"\[0.5", - name="feature_demo", + spec_file_name="feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="expanded", negate=True, @@ -715,7 +722,8 @@ def define_tests(): # pylint: disable=R0914,R0915 HasReturnCode(), ProvenanceYAMLFileHasRegex( regex="cli_test_demo_workers:", - name="feature_demo", + spec_file_name="remote_feature_demo", + study_name="feature_demo", output_path=OUTPUT_DIR, provenance_type="expanded", ), diff --git a/tests/unit/study/test_study.py b/tests/unit/study/test_study.py index a00995d55..cb15805cd 100644 --- a/tests/unit/study/test_study.py +++ b/tests/unit/study/test_study.py @@ -41,6 +41,15 @@ nodes: 1 task_queue: hello_queue + - name: test_special_vars + description: test the special vars + run: + cmd: | + echo $(MERLIN_SPEC_ORIGINAL_TEMPLATE) + echo $(MERLIN_SPEC_EXECUTED_RUN) + echo $(MERLIN_SPEC_ARCHIVED_COPY) + task_queue: special_var_queue + global.parameters: X2: values : [0.5] @@ -234,11 +243,16 @@ class TestMerlinStudy(unittest.TestCase): @staticmethod def file_contains_string(f, string): - return string in open(f, "r").read() + result = False + with open(f, "r") as infile: + if string in infile.read(): + result = True + return result def setUp(self): self.tmpdir = tempfile.mkdtemp() - self.merlin_spec_filepath = os.path.join(self.tmpdir, "basic_ensemble.yaml") + self.base_name = "basic_ensemble" + self.merlin_spec_filepath = os.path.join(self.tmpdir, f"{self.base_name}.yaml") with open(self.merlin_spec_filepath, "w+") as _file: _file.write(MERLIN_SPEC) @@ -263,6 +277,34 @@ def test_expanded_spec(self): assert TestMerlinStudy.file_contains_string(self.study.expanded_spec.path, "$PATH") assert not TestMerlinStudy.file_contains_string(self.study.expanded_spec.path, "PATH_VAR: $PATH") + # Special vars are in the second step of MERLIN_SPEC so grab that step here + original_special_var_step = self.study.original_spec.study[1]["run"]["cmd"] + expanded_special_var_step = self.study.expanded_spec.study[1]["run"]["cmd"] + + # Make sure the special filepath variables aren't expanded in the original spec + assert "$(MERLIN_SPEC_ORIGINAL_TEMPLATE)" in original_special_var_step + assert "$(MERLIN_SPEC_EXECUTED_RUN)" in original_special_var_step + assert "$(MERLIN_SPEC_ARCHIVED_COPY)" in original_special_var_step + + # Make sure the special filepath variables aren't left in their variable form in the expanded spec + assert "$(MERLIN_SPEC_ORIGINAL_TEMPLATE)" not in expanded_special_var_step + assert "$(MERLIN_SPEC_EXECUTED_RUN)" not in expanded_special_var_step + assert "$(MERLIN_SPEC_ARCHIVED_COPY)" not in expanded_special_var_step + + # Make sure the special filepath variables we're expanded appropriately in the expanded spec + assert ( + f"{self.base_name}.orig.yaml" in expanded_special_var_step + and "unit_test1.orig.yaml" not in expanded_special_var_step + ) + assert ( + f"{self.base_name}.partial.yaml" in expanded_special_var_step + and "unit_test1.partial.yaml" not in expanded_special_var_step + ) + assert ( + f"{self.base_name}.expanded.yaml" in expanded_special_var_step + and "unit_test1.expanded.yaml" not in expanded_special_var_step + ) + def test_column_label_conflict(self): """ If there is a common key between Maestro's global.parameters and @@ -291,3 +333,7 @@ def test_no_env(self): assert isinstance(study_no_env, MerlinStudy), bad_type_err except Exception as e: assert False, f"Encountered unexpected exception, {e}, for viable MerlinSpec without optional 'env' section." + + +if __name__ == "__main__": + unittest.main() From b8dd2b2bc69657601b49e07a026f9e1d3c74fc71 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 26 May 2023 08:25:54 -0700 Subject: [PATCH 04/48] Create dependabot-changelog-updater.yml --- .../dependabot-changelog-updater.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/dependabot-changelog-updater.yml diff --git a/.github/workflows/dependabot-changelog-updater.yml b/.github/workflows/dependabot-changelog-updater.yml new file mode 100644 index 000000000..1677cf050 --- /dev/null +++ b/.github/workflows/dependabot-changelog-updater.yml @@ -0,0 +1,34 @@ +# See https://github.com/dangoslen/dependabot-changelog-helper for more info +name: Dependabot Changelog Updater +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + - labeled + - unlabeled + +jobs: + changelog: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - uses: actions/checkout@v3 + with: + # Depending on your needs, you can use a token that will re-trigger workflows + # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: dangoslen/dependabot-changelog-helper@v3 + with: + version: ${{ needs.setup.outputs.version }} + activationLabel: 'dependabot' + changelogPath: './CHANGELOG.md' + + # This step is required for committing the changes to your branch. + # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "Updated Changelog" From 4c404235c6c5cf775a9db9606413c4a9cfb0fb52 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 26 May 2023 08:40:45 -0700 Subject: [PATCH 05/48] testing outputs of modifying changelog --- .../dependabot-changelog-updater.yml | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dependabot-changelog-updater.yml b/.github/workflows/dependabot-changelog-updater.yml index 1677cf050..a8afccc92 100644 --- a/.github/workflows/dependabot-changelog-updater.yml +++ b/.github/workflows/dependabot-changelog-updater.yml @@ -11,24 +11,30 @@ on: - unlabeled jobs: - changelog: + changelog-updater: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - - uses: actions/checkout@v3 - with: - # Depending on your needs, you can use a token that will re-trigger workflows - # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs - token: ${{ secrets.GITHUB_TOKEN }} +# - uses: actions/checkout@v3 +# with: +# # Depending on your needs, you can use a token that will re-trigger workflows +# # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs +# token: ${{ secrets.GITHUB_TOKEN }} - - uses: dangoslen/dependabot-changelog-helper@v3 + - name: modify changelog + id: modify-changelog + uses: dangoslen/dependabot-changelog-helper@v3 with: version: ${{ needs.setup.outputs.version }} activationLabel: 'dependabot' changelogPath: './CHANGELOG.md' + + - name: commit changes + run: | + echo ${{ steps.modify-changelog.outputs }} # This step is required for committing the changes to your branch. # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs - - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: "Updated Changelog" +# - uses: stefanzweifel/git-auto-commit-action@v4 +# with: +# commit_message: "Updated Changelog" From 2d231fb937303839934d4ee2048d554252ce9dbd Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 26 May 2023 09:13:57 -0700 Subject: [PATCH 06/48] delete dependabot-changelog-updater --- .../dependabot-changelog-updater.yml | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/dependabot-changelog-updater.yml diff --git a/.github/workflows/dependabot-changelog-updater.yml b/.github/workflows/dependabot-changelog-updater.yml deleted file mode 100644 index a8afccc92..000000000 --- a/.github/workflows/dependabot-changelog-updater.yml +++ /dev/null @@ -1,40 +0,0 @@ -# See https://github.com/dangoslen/dependabot-changelog-helper for more info -name: Dependabot Changelog Updater -on: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - - labeled - - unlabeled - -jobs: - changelog-updater: - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} - steps: -# - uses: actions/checkout@v3 -# with: -# # Depending on your needs, you can use a token that will re-trigger workflows -# # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs -# token: ${{ secrets.GITHUB_TOKEN }} - - - name: modify changelog - id: modify-changelog - uses: dangoslen/dependabot-changelog-helper@v3 - with: - version: ${{ needs.setup.outputs.version }} - activationLabel: 'dependabot' - changelogPath: './CHANGELOG.md' - - - name: commit changes - run: | - echo ${{ steps.modify-changelog.outputs }} - - # This step is required for committing the changes to your branch. - # See https://github.com/stefanzweifel/git-auto-commit-action#commits-of-this-action-do-not-trigger-new-workflow-runs -# - uses: stefanzweifel/git-auto-commit-action@v4 -# with: -# commit_message: "Updated Changelog" From 6b142d9b0f67f2cca6856b722b96114ef7a2ca7b Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Wed, 28 Jun 2023 07:56:45 -0700 Subject: [PATCH 07/48] feature/pdf-docs (#427) * first attempt at adding pdf * fixing build error * modify changelog to show docs changes * fix errors Luc found in the build logs * trying out removal of latex * reverting latex changes back * uncommenting the latex_elements settings * adding epub to see if latex will build * adding a latex engine variable to conf * fix naming error with latex_engine * attempting to add a logo to the pdf build * testing an override to the searchtools file * revert back to not using searchtools override * update changelog --- .readthedocs.yaml | 2 ++ CHANGELOG.md | 3 +++ docs/source/conf.py | 13 ++++++++----- docs/source/faq.rst | 2 +- docs/source/merlin_developer.rst | 3 +-- docs/source/merlin_variables.rst | 2 +- docs/source/modules/installation/installation.rst | 2 +- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c1c252e30..ee62bcb5e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,3 +11,5 @@ sphinx: python: install: - requirements: docs/requirements.txt + +formats: [pdf] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 603086aaa..25ee73963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Fixed - A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name +- Some build warnings in the docs (unknown targets, duplicate targets, title underlines too short, etc.) ### Added - Tests for ensuring `$(MERLIN_SPEC_ORIGINAL_TEMPLATE)`, `$(MERLIN_SPEC_ARCHIVED_COPY)`, and `$(MERLIN_SPEC_EXECUTED_RUN)` are stored correctly +- A pdf download format for the docs ### Changed - The ProvenanceYAMLFileHasRegex condition for integration tests now saves the study name and spec file name as attributes instead of just the study name - This lead to minor changes in 3 tests ("local override feature demo", "local pgen feature demo", and "remote feature demo") with what we pass to this specific condition +- Uncommented Latex support in the docs configuration to get pdf builds working ## [1.10.1] ### Fixed diff --git a/docs/source/conf.py b/docs/source/conf.py index 315978a6a..b578e8672 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -66,7 +66,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -122,21 +122,24 @@ # -- Options for LaTeX output ------------------------------------------------ +latex_engine = "pdflatex" latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # - # 'papersize': 'letterpaper', + 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # - # 'pointsize': '10pt', + 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # - # 'preamble': '', + 'preamble': '', # Latex figure (float) alignment # - # 'figure_align': 'htbp', + 'figure_align': 'htbp', } +latex_logo = "../images/merlin.png" + # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). diff --git a/docs/source/faq.rst b/docs/source/faq.rst index d0ef8e109..3632aab6c 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -383,7 +383,7 @@ in the ``launch_args`` variable in the batch section. What is PBS? ~~~~~~~~~~~~ Another job scheduler. See `Portable Batch System -https://en.wikipedia.org/wiki/Portable_Batch_System`_ +`_ . This functionality is only available to launch a flux scheduler. diff --git a/docs/source/merlin_developer.rst b/docs/source/merlin_developer.rst index 08947ca89..e88352f3e 100644 --- a/docs/source/merlin_developer.rst +++ b/docs/source/merlin_developer.rst @@ -164,8 +164,7 @@ properties labeled ``name`` and ``population`` that are both required, it would } Here, ``name`` can only be a string but ``population`` can be both a string and an integer. -For help with json schema formatting, check out the `step-by-step getting started guide -`_. +For help with json schema formatting, check out the `step-by-step getting started guide`_. The next step is to enable this block in the schema validation process. To do this we need to: diff --git a/docs/source/merlin_variables.rst b/docs/source/merlin_variables.rst index 7f545a4d2..d67e06412 100644 --- a/docs/source/merlin_variables.rst +++ b/docs/source/merlin_variables.rst @@ -161,7 +161,7 @@ Reserved variables $(MERLIN_INFO)/*.expanded.yaml The ``LAUNCHER`` Variable -+++++++++++++++++++++ ++++++++++++++++++++++++++ ``$(LAUNCHER)`` is a special case of a reserved variable since it's value *can* be changed. It serves as an abstraction to launch a job with parallel schedulers like :ref:`slurm`, diff --git a/docs/source/modules/installation/installation.rst b/docs/source/modules/installation/installation.rst index d18261af5..96195ff3d 100644 --- a/docs/source/modules/installation/installation.rst +++ b/docs/source/modules/installation/installation.rst @@ -229,7 +229,7 @@ If everything is set up correctly, you should see: (OPTIONAL) Docker Advanced Installation ----------------------------- +--------------------------------------- RabbitMQ Server +++++++++++++++ From e3e1a307d6c65fcc834fe4a3aba2271f9109932d Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:18:25 -0700 Subject: [PATCH 08/48] bugfix/openfoam_singularity_issues (#426) * fix openfoam_singularity issues * update requirements and descriptions for openfoam examples --- CHANGELOG.md | 2 ++ merlin/examples/generator.py | 2 ++ merlin/examples/workflows/openfoam_wf/openfoam_wf.yaml | 1 + merlin/examples/workflows/openfoam_wf/openfoam_wf_template.yaml | 1 + merlin/examples/workflows/openfoam_wf/requirements.txt | 2 +- .../workflows/openfoam_wf_no_docker/openfoam_wf_no_docker.yaml | 1 + .../openfoam_wf_no_docker/openfoam_wf_no_docker_template.yaml | 1 + .../examples/workflows/openfoam_wf_no_docker/requirements.txt | 2 +- .../{openfoam_wf.yaml => openfoam_wf_singularity.yaml} | 1 + .../examples/workflows/openfoam_wf_singularity/requirements.txt | 2 +- 10 files changed, 12 insertions(+), 3 deletions(-) rename merlin/examples/workflows/openfoam_wf_singularity/{openfoam_wf.yaml => openfoam_wf_singularity.yaml} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ee73963..ebaea86f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Fixed - A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name +- A bug where the openfoam_wf_singularity example was not being found - Some build warnings in the docs (unknown targets, duplicate targets, title underlines too short, etc.) ### Added @@ -16,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The ProvenanceYAMLFileHasRegex condition for integration tests now saves the study name and spec file name as attributes instead of just the study name - This lead to minor changes in 3 tests ("local override feature demo", "local pgen feature demo", and "remote feature demo") with what we pass to this specific condition +- Updated scikit-learn requirement for the openfoam_wf_singularity example - Uncommented Latex support in the docs configuration to get pdf builds working ## [1.10.1] diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index b859ff605..308785784 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -83,6 +83,8 @@ def list_examples(): directory = os.path.join(os.path.join(EXAMPLES_DIR, example_dir), "") specs = glob.glob(directory + "*.yaml") for spec in specs: + if "template" in spec: + continue with open(spec) as f: # pylint: disable=C0103 try: spec_metadata = yaml.safe_load(f)["description"] diff --git a/merlin/examples/workflows/openfoam_wf/openfoam_wf.yaml b/merlin/examples/workflows/openfoam_wf/openfoam_wf.yaml index e6dabc8c2..0d98445c0 100644 --- a/merlin/examples/workflows/openfoam_wf/openfoam_wf.yaml +++ b/merlin/examples/workflows/openfoam_wf/openfoam_wf.yaml @@ -3,6 +3,7 @@ description: description: | A parameter study that includes initializing, running, post-processing, collecting, learning and visualizing OpenFOAM runs + using docker. env: diff --git a/merlin/examples/workflows/openfoam_wf/openfoam_wf_template.yaml b/merlin/examples/workflows/openfoam_wf/openfoam_wf_template.yaml index 96e13064b..64433d3af 100644 --- a/merlin/examples/workflows/openfoam_wf/openfoam_wf_template.yaml +++ b/merlin/examples/workflows/openfoam_wf/openfoam_wf_template.yaml @@ -3,6 +3,7 @@ description: description: | A parameter study that includes initializing, running, post-processing, collecting, learning and visualizing OpenFOAM runs + using docker. env: diff --git a/merlin/examples/workflows/openfoam_wf/requirements.txt b/merlin/examples/workflows/openfoam_wf/requirements.txt index 8042c2422..ef63ca016 100644 --- a/merlin/examples/workflows/openfoam_wf/requirements.txt +++ b/merlin/examples/workflows/openfoam_wf/requirements.txt @@ -1,3 +1,3 @@ Ofpp==0.11 -scikit-learn==0.21.3 +scikit-learn>=1.0.2 matplotlib==3.1.1 diff --git a/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker.yaml b/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker.yaml index ab8224029..688e8ceff 100644 --- a/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker.yaml +++ b/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker.yaml @@ -3,6 +3,7 @@ description: description: | A parameter study that includes initializing, running, post-processing, collecting, learning and vizualizing OpenFOAM runs + without using docker. env: diff --git a/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker_template.yaml b/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker_template.yaml index 084a1c0bb..3fcbc3588 100644 --- a/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker_template.yaml +++ b/merlin/examples/workflows/openfoam_wf_no_docker/openfoam_wf_no_docker_template.yaml @@ -3,6 +3,7 @@ description: description: | A parameter study that includes initializing, running, post-processing, collecting, learning and vizualizing OpenFOAM runs + without using docker. env: diff --git a/merlin/examples/workflows/openfoam_wf_no_docker/requirements.txt b/merlin/examples/workflows/openfoam_wf_no_docker/requirements.txt index 8042c2422..ef63ca016 100644 --- a/merlin/examples/workflows/openfoam_wf_no_docker/requirements.txt +++ b/merlin/examples/workflows/openfoam_wf_no_docker/requirements.txt @@ -1,3 +1,3 @@ Ofpp==0.11 -scikit-learn==0.21.3 +scikit-learn>=1.0.2 matplotlib==3.1.1 diff --git a/merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf.yaml b/merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf_singularity.yaml similarity index 99% rename from merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf.yaml rename to merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf_singularity.yaml index 3f3bbb672..03837bcb3 100644 --- a/merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf.yaml +++ b/merlin/examples/workflows/openfoam_wf_singularity/openfoam_wf_singularity.yaml @@ -3,6 +3,7 @@ description: description: | A parameter study that includes initializing, running, post-processing, collecting, learning and visualizing OpenFOAM runs + using singularity. env: diff --git a/merlin/examples/workflows/openfoam_wf_singularity/requirements.txt b/merlin/examples/workflows/openfoam_wf_singularity/requirements.txt index 8042c2422..ef63ca016 100644 --- a/merlin/examples/workflows/openfoam_wf_singularity/requirements.txt +++ b/merlin/examples/workflows/openfoam_wf_singularity/requirements.txt @@ -1,3 +1,3 @@ Ofpp==0.11 -scikit-learn==0.21.3 +scikit-learn>=1.0.2 matplotlib==3.1.1 From 12ff3d782d30579b1387eb314f33924d33d1877f Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:26:15 -0700 Subject: [PATCH 09/48] bugfix/output-path-substitution (#430) * fix bug with output_path and variable substitution * add tests for cli substitutions --- CHANGELOG.md | 2 ++ merlin/study/study.py | 15 +++++++++++-- tests/integration/test_definitions.py | 22 +++++++++++++++++++ .../test_specs/cli_substitution_test.yaml | 14 ++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/integration/test_specs/cli_substitution_test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index ebaea86f2..d1452104b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name - A bug where the openfoam_wf_singularity example was not being found - Some build warnings in the docs (unknown targets, duplicate targets, title underlines too short, etc.) +- A bug where when the output path contained a variable that was overridden, the overridden variable was not changed in the output_path ### Added - Tests for ensuring `$(MERLIN_SPEC_ORIGINAL_TEMPLATE)`, `$(MERLIN_SPEC_ARCHIVED_COPY)`, and `$(MERLIN_SPEC_EXECUTED_RUN)` are stored correctly - A pdf download format for the docs +- Tests for cli substitutions ### Changed - The ProvenanceYAMLFileHasRegex condition for integration tests now saves the study name and spec file name as attributes instead of just the study name diff --git a/merlin/study/study.py b/merlin/study/study.py index 5446c84a1..74b7c3181 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -306,8 +306,17 @@ def output_path(self): output_path = str(self.original_spec.output_path) - if (self.override_vars is not None) and ("OUTPUT_PATH" in self.override_vars): - output_path = str(self.override_vars["OUTPUT_PATH"]) + # If there are override vars we need to check that the output path doesn't need changed + if self.override_vars is not None: + # Case where output path is directly modified + if "OUTPUT_PATH" in self.override_vars: + output_path = str(self.override_vars["OUTPUT_PATH"]) + else: + for var_name, var_val in self.override_vars.items(): + token = f"$({var_name})" + # Case where output path contains a variable that was overridden + if token in output_path: + output_path = output_path.replace(token, str(var_val)) output_path = expand_line(output_path, self.user_vars, env_vars=True) output_path = os.path.abspath(output_path) @@ -315,6 +324,8 @@ def output_path(self): os.makedirs(output_path) LOG.info(f"Made dir(s) to output path '{output_path}'.") + LOG.info(f"OUTPUT_PATH: {os.path.basename(output_path)}") + return output_path @cached_property diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index cf7c008de..d20efb329 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -127,6 +127,7 @@ def define_tests(): # pylint: disable=R0914,R0915 flux_native = f"{test_specs}/flux_par_native_test.yaml" lsf = f"{examples}/lsf/lsf_par.yaml" mul_workers_demo = f"{dev_examples}/multiple_workers.yaml" + cli_substitution_wf = f"{test_specs}/cli_substitution_test.yaml" # Other shortcuts black = "black --check --target-version py36" @@ -323,6 +324,26 @@ def define_tests(): # pylint: disable=R0914,R0915 "run type": "local", }, } + cli_substitution_tests = { + "no substitutions": { + "cmds": f"merlin run {cli_substitution_wf} --local", + "conditions": [HasReturnCode(), HasRegex(r"OUTPUT_PATH: output_path_no_substitution")], + "run type": "local", + "cleanup": "rm -r output_path_no_substitution", + }, + "output_path substitution": { + "cmds": f"merlin run {cli_substitution_wf} --local --vars OUTPUT_PATH=output_path_substitution", + "conditions": [HasReturnCode(), HasRegex(r"OUTPUT_PATH: output_path_substitution")], + "run type": "local", + "cleanup": "rm -r output_path_substitution", + }, + "output_path w/ variable substitution": { + "cmds": f"merlin run {cli_substitution_wf} --local --vars SUB=variable_sub", + "conditions": [HasReturnCode(), HasRegex(r"OUTPUT_PATH: output_path_variable_sub")], + "run type": "local", + "cleanup": "rm -r output_path_variable_sub", + }, + } example_tests = { "example failure": {"cmds": "merlin example failure", "conditions": HasRegex("not found"), "run type": "local"}, "example simple_chain": { @@ -748,6 +769,7 @@ def define_tests(): # pylint: disable=R0914,R0915 examples_check, run_workers_echo_tests, wf_format_tests, + cli_substitution_tests, example_tests, restart_step_tests, restart_wf_tests, diff --git a/tests/integration/test_specs/cli_substitution_test.yaml b/tests/integration/test_specs/cli_substitution_test.yaml new file mode 100644 index 000000000..5cbbb70d3 --- /dev/null +++ b/tests/integration/test_specs/cli_substitution_test.yaml @@ -0,0 +1,14 @@ +description: + name: cli_substitution_test + description: a spec that helps test cli substitutions + +env: + variables: + SUB: no_substitution + OUTPUT_PATH: output_path_$(SUB) + +study: + - name: step1 + description: step 1 + run: + cmd: echo "test" From 5c69c0beaf74962abd585ea321a6f071d28ad881 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:28:55 -0700 Subject: [PATCH 10/48] bugfix/scheduler-permission-error (#436) --- CHANGELOG.md | 1 + merlin/study/batch.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1452104b..981ce7198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A bug where the openfoam_wf_singularity example was not being found - Some build warnings in the docs (unknown targets, duplicate targets, title underlines too short, etc.) - A bug where when the output path contained a variable that was overridden, the overridden variable was not changed in the output_path +- A bug where permission denied errors happened when checking for system scheduler ### Added - Tests for ensuring `$(MERLIN_SPEC_ORIGINAL_TEMPLATE)`, `$(MERLIN_SPEC_ARCHIVED_COPY)`, and `$(MERLIN_SPEC_EXECUTED_RUN)` are stored correctly diff --git a/merlin/study/batch.py b/merlin/study/batch.py index eeaead5ee..f5b62409f 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -92,7 +92,7 @@ def check_for_scheduler(scheduler, scheduler_legend): if result and len(result) > 0 and scheduler_legend[scheduler]["expected check output"] in result[0]: return True return False - except FileNotFoundError: + except (FileNotFoundError, PermissionError): return False From c01f6358402ee7c20a486560adf2e8f0591150b5 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:04:11 -0700 Subject: [PATCH 11/48] Release/1.10.2 (#437) * bump version to 1.10.2 * bump version in CHANGELOG --- .gitignore | 1 + CHANGELOG.md | 2 +- Makefile | 2 +- merlin/__init__.py | 4 ++-- merlin/ascii_art.py | 2 +- merlin/celery.py | 2 +- merlin/common/__init__.py | 2 +- merlin/common/abstracts/__init__.py | 2 +- merlin/common/abstracts/enums/__init__.py | 2 +- merlin/common/openfilelist.py | 2 +- merlin/common/opennpylib.py | 2 +- merlin/common/sample_index.py | 2 +- merlin/common/sample_index_factory.py | 2 +- merlin/common/security/__init__.py | 2 +- merlin/common/security/encrypt.py | 2 +- merlin/common/security/encrypt_backend_traffic.py | 2 +- merlin/common/tasks.py | 2 +- merlin/common/util_sampling.py | 2 +- merlin/config/__init__.py | 2 +- merlin/config/broker.py | 2 +- merlin/config/celeryconfig.py | 2 +- merlin/config/configfile.py | 2 +- merlin/config/results_backend.py | 2 +- merlin/config/utils.py | 2 +- merlin/data/celery/__init__.py | 2 +- merlin/display.py | 2 +- merlin/examples/__init__.py | 2 +- merlin/examples/examples.py | 2 +- merlin/examples/generator.py | 2 +- merlin/exceptions/__init__.py | 2 +- merlin/log_formatter.py | 2 +- merlin/main.py | 2 +- merlin/merlin_templates.py | 2 +- merlin/router.py | 2 +- merlin/server/__init__.py | 2 +- merlin/server/server_commands.py | 2 +- merlin/server/server_config.py | 2 +- merlin/server/server_util.py | 2 +- merlin/spec/__init__.py | 2 +- merlin/spec/all_keys.py | 2 +- merlin/spec/defaults.py | 2 +- merlin/spec/expansion.py | 2 +- merlin/spec/override.py | 2 +- merlin/spec/specification.py | 2 +- merlin/study/__init__.py | 2 +- merlin/study/batch.py | 2 +- merlin/study/celeryadapter.py | 2 +- merlin/study/dag.py | 2 +- merlin/study/script_adapter.py | 2 +- merlin/study/step.py | 2 +- merlin/study/study.py | 2 +- merlin/utils.py | 2 +- setup.py | 2 +- tests/integration/conditions.py | 2 +- tests/integration/run_tests.py | 2 +- tests/integration/test_definitions.py | 2 +- 56 files changed, 57 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 8b0fb8ad2..c22521934 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ jupyter/testDistributedSamples.py dist/ build/ .DS_Store +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 981ce7198..5e7c0b1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] +## [1.10.2] ### Fixed - A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name - A bug where the openfoam_wf_singularity example was not being found diff --git a/Makefile b/Makefile index 0153f10d4..030ed8d15 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/__init__.py b/merlin/__init__.py index 32bf5c875..9856d9a7b 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # @@ -38,7 +38,7 @@ import sys -__version__ = "1.10.1" +__version__ = "1.10.2" VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/ascii_art.py b/merlin/ascii_art.py index bb804d876..5d61d8ed1 100644 --- a/merlin/ascii_art.py +++ b/merlin/ascii_art.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/celery.py b/merlin/celery.py index 072c83b58..dc0cfd9d7 100644 --- a/merlin/celery.py +++ b/merlin/celery.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/__init__.py b/merlin/common/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/common/__init__.py +++ b/merlin/common/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/__init__.py b/merlin/common/abstracts/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/common/abstracts/__init__.py +++ b/merlin/common/abstracts/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/enums/__init__.py b/merlin/common/abstracts/enums/__init__.py index 81c8865e5..be663a572 100644 --- a/merlin/common/abstracts/enums/__init__.py +++ b/merlin/common/abstracts/enums/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/openfilelist.py b/merlin/common/openfilelist.py index f51055ab5..a4c127a12 100644 --- a/merlin/common/openfilelist.py +++ b/merlin/common/openfilelist.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/opennpylib.py b/merlin/common/opennpylib.py index 65d503564..f8b881dcf 100644 --- a/merlin/common/opennpylib.py +++ b/merlin/common/opennpylib.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index.py b/merlin/common/sample_index.py index 635dd617b..821c1df71 100644 --- a/merlin/common/sample_index.py +++ b/merlin/common/sample_index.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index_factory.py b/merlin/common/sample_index_factory.py index eb24e067e..fe9d735d4 100644 --- a/merlin/common/sample_index_factory.py +++ b/merlin/common/sample_index_factory.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/__init__.py b/merlin/common/security/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/common/security/__init__.py +++ b/merlin/common/security/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt.py b/merlin/common/security/encrypt.py index 22c505df4..dfd230a1a 100644 --- a/merlin/common/security/encrypt.py +++ b/merlin/common/security/encrypt.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt_backend_traffic.py b/merlin/common/security/encrypt_backend_traffic.py index 4d7dc176f..2c70fadc9 100644 --- a/merlin/common/security/encrypt_backend_traffic.py +++ b/merlin/common/security/encrypt_backend_traffic.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index 3b41cb459..b24e0af3e 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/util_sampling.py b/merlin/common/util_sampling.py index 43dbd133a..1f851abb2 100644 --- a/merlin/common/util_sampling.py +++ b/merlin/common/util_sampling.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/__init__.py b/merlin/config/__init__.py index aee1b62b0..a53190df7 100644 --- a/merlin/config/__init__.py +++ b/merlin/config/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/broker.py b/merlin/config/broker.py index 0cf9b7c6d..8c7dc8a2b 100644 --- a/merlin/config/broker.py +++ b/merlin/config/broker.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/celeryconfig.py b/merlin/config/celeryconfig.py index 2740aa0a1..73605768e 100644 --- a/merlin/config/celeryconfig.py +++ b/merlin/config/celeryconfig.py @@ -10,7 +10,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/configfile.py b/merlin/config/configfile.py index 6aa38f9b2..6ccf3dea5 100644 --- a/merlin/config/configfile.py +++ b/merlin/config/configfile.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index 691bf5a6c..d30d5ab58 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/utils.py b/merlin/config/utils.py index 9778cde3c..2f5434091 100644 --- a/merlin/config/utils.py +++ b/merlin/config/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/data/celery/__init__.py b/merlin/data/celery/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/data/celery/__init__.py +++ b/merlin/data/celery/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/display.py b/merlin/display.py index 89d14781e..bf60bb50d 100644 --- a/merlin/display.py +++ b/merlin/display.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/__init__.py b/merlin/examples/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/examples/__init__.py +++ b/merlin/examples/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/examples.py b/merlin/examples/examples.py index a66abf64a..a7754cec7 100644 --- a/merlin/examples/examples.py +++ b/merlin/examples/examples.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index 308785784..5b893ab9e 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index cfbc21e38..b176378a9 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/log_formatter.py b/merlin/log_formatter.py index 580952b1d..e90e13324 100644 --- a/merlin/log_formatter.py +++ b/merlin/log_formatter.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/main.py b/merlin/main.py index f49d3cc94..d3fe4b80a 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/merlin_templates.py b/merlin/merlin_templates.py index 4809c7aee..b2bcc4949 100644 --- a/merlin/merlin_templates.py +++ b/merlin/merlin_templates.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/router.py b/merlin/router.py index 160909ddf..779333dc6 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/__init__.py b/merlin/server/__init__.py index 7c6148fee..6cd46fa97 100644 --- a/merlin/server/__init__.py +++ b/merlin/server/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index d53463c2b..93f928f3b 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_config.py b/merlin/server/server_config.py index 14abfb0d4..e088eedcd 100644 --- a/merlin/server/server_config.py +++ b/merlin/server/server_config.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_util.py b/merlin/server/server_util.py index d101af2f0..a3cc7f021 100644 --- a/merlin/server/server_util.py +++ b/merlin/server/server_util.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/__init__.py b/merlin/spec/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/spec/__init__.py +++ b/merlin/spec/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/all_keys.py b/merlin/spec/all_keys.py index f58731336..145eb00bb 100644 --- a/merlin/spec/all_keys.py +++ b/merlin/spec/all_keys.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index b20ce7d09..1560f412d 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/expansion.py b/merlin/spec/expansion.py index 38f03d6e9..abba6ef02 100644 --- a/merlin/spec/expansion.py +++ b/merlin/spec/expansion.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/override.py b/merlin/spec/override.py index 0f1e71f62..e9ec4bceb 100644 --- a/merlin/spec/override.py +++ b/merlin/spec/override.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 8d36297ab..1cb26d512 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/__init__.py b/merlin/study/__init__.py index 1e9c75683..05c5d0bbf 100644 --- a/merlin/study/__init__.py +++ b/merlin/study/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/batch.py b/merlin/study/batch.py index f5b62409f..b2157ebde 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index 4c01cfd73..f9c04b1e3 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/dag.py b/merlin/study/dag.py index a85a86e47..d2315bf7f 100644 --- a/merlin/study/dag.py +++ b/merlin/study/dag.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index 2df053051..dc01ad6a9 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/step.py b/merlin/study/step.py index 14f273dfa..686b5afa2 100644 --- a/merlin/study/step.py +++ b/merlin/study/step.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/study.py b/merlin/study/study.py index 74b7c3181..8eaf306ca 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/utils.py b/merlin/utils.py index 7c18407e7..a2e4966d0 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/setup.py b/setup.py index 9693b06ba..0bb413325 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index 4da8c36a1..0bc687923 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index 9b0270d3b..82a4fdd92 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index d20efb329..e9e9ce590 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.1. +# This file is part of Merlin, Version: 1.10.2. # # For details, see https://github.com/LLNL/merlin. # From 9c52ba2799e52f713f622ece8139f67a2d2b04d3 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:04:11 -0700 Subject: [PATCH 12/48] resolve develop to main merge issues (#439) * fix default worker bug with all steps * version bump and requirements fix From b0f4d866e57b2edb324d5773a041122a4f1930d3 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:37:08 -0700 Subject: [PATCH 13/48] dependabot/certifi-requests-pygments (#441) * Bump certifi from 2022.12.7 to 2023.7.22 in /docs Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] * add all dependabot changes and update CHANGELOG --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 ++++++++++ docs/requirements.txt | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7c0b1cd..b3ac21def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] +### Fixed + +### Added + +### Changed +- Bump certifi from 2022.12.7 to 2023.7.22 in /docs +- Bump pygments from 2.13.0 to 2.15.0 in /docs +- Bump requests from 2.28.1 to 2.31.0 in /docs + ## [1.10.2] ### Fixed - A bug where the .orig, .partial, and .expanded file names were using the study name rather than the original file name diff --git a/docs/requirements.txt b/docs/requirements.txt index 87333eb50..c771e60dc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,7 +8,7 @@ alabaster==0.7.12 # via sphinx babel==2.10.3 # via sphinx -certifi==2022.12.7 +certifi==2023.7.22 # via requests charset-normalizer==2.1.1 # via requests @@ -26,13 +26,13 @@ markupsafe==2.1.1 # via jinja2 packaging==21.3 # via sphinx -pygments==2.13.0 +pygments==2.15.0 # via sphinx pyparsing==3.0.9 # via packaging pytz==2022.5 # via babel -requests==2.28.1 +requests==2.31.0 # via sphinx snowballstemmer==2.2.0 # via sphinx From c641c5c6495ebc0e789019b8ff80e5d0419ff597 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 18 Aug 2023 13:02:19 -0700 Subject: [PATCH 14/48] bugfix/server-pip-redis-conf (#443) * add *.conf to the MANIFEST file so pip will grab the redis.conf file * add note explaining how to fix a hanging merlin server start * modify CHANGELOG * add second export option to docs and fix typo --- CHANGELOG.md | 2 ++ MANIFEST.in | 2 +- docs/source/merlin_server.rst | 2 +- docs/source/server/commands.rst | 7 +++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ac21def..f5e60ea9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Added +- The *.conf regex for the recursive-include of the merlin server directory so that pip will add it to the wheel +- A note to the docs for how to fix an issue where the `merlin server start` command hangs ### Changed - Bump certifi from 2022.12.7 to 2023.7.22 in /docs diff --git a/MANIFEST.in b/MANIFEST.in index da9d411ad..d5526e37c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ recursive-include merlin/data *.yaml *.py -recursive-include merlin/server *.yaml *.py +recursive-include merlin/server *.yaml *.py *.conf recursive-include merlin/examples * include requirements.txt include requirements/* diff --git a/docs/source/merlin_server.rst b/docs/source/merlin_server.rst index 24b37c776..23f6d4a1d 100644 --- a/docs/source/merlin_server.rst +++ b/docs/source/merlin_server.rst @@ -1,7 +1,7 @@ Merlin Server ============= The merlin server command allows users easy access to containerized broker -and results servers for merlin workflows. This allowsusers to run merlin without +and results servers for merlin workflows. This allows users to run merlin without a dedicated external server. The main configuration will be stored in the subdirectory called "server/" by diff --git a/docs/source/server/commands.rst b/docs/source/server/commands.rst index dd8ca1b02..fc40bf182 100644 --- a/docs/source/server/commands.rst +++ b/docs/source/server/commands.rst @@ -34,6 +34,13 @@ Starting up a Merlin Server (``merlin server start``) Starts the container located in the local merlin server configuration. +.. note:: + If this command seems to hang and never release control back to you, follow these steps: + + 1. Kill the command with ``Ctrl+C`` + 2. Run either ``export LC_ALL="C.UTF-8"`` or ``export LC_ALL="C"`` + 3. Re-run the ``merlin server start`` command + Stopping an exisiting Merlin Server (``merlin server stop``) ------------------------------------------------------------ From 970a06fd6e4ba927b44180ad3bb4819df0fbdb73 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:20:26 -0700 Subject: [PATCH 15/48] bump to version 1.10.3 (#444) --- CHANGELOG.md | 4 +--- Makefile | 2 +- merlin/__init__.py | 4 ++-- merlin/ascii_art.py | 2 +- merlin/celery.py | 2 +- merlin/common/__init__.py | 2 +- merlin/common/abstracts/__init__.py | 2 +- merlin/common/abstracts/enums/__init__.py | 2 +- merlin/common/openfilelist.py | 2 +- merlin/common/opennpylib.py | 2 +- merlin/common/sample_index.py | 2 +- merlin/common/sample_index_factory.py | 2 +- merlin/common/security/__init__.py | 2 +- merlin/common/security/encrypt.py | 2 +- merlin/common/security/encrypt_backend_traffic.py | 2 +- merlin/common/tasks.py | 2 +- merlin/common/util_sampling.py | 2 +- merlin/config/__init__.py | 2 +- merlin/config/broker.py | 2 +- merlin/config/celeryconfig.py | 2 +- merlin/config/configfile.py | 2 +- merlin/config/results_backend.py | 2 +- merlin/config/utils.py | 2 +- merlin/data/celery/__init__.py | 2 +- merlin/display.py | 2 +- merlin/examples/__init__.py | 2 +- merlin/examples/examples.py | 2 +- merlin/examples/generator.py | 2 +- merlin/exceptions/__init__.py | 2 +- merlin/log_formatter.py | 2 +- merlin/main.py | 2 +- merlin/merlin_templates.py | 2 +- merlin/router.py | 2 +- merlin/server/__init__.py | 2 +- merlin/server/server_commands.py | 2 +- merlin/server/server_config.py | 2 +- merlin/server/server_util.py | 2 +- merlin/spec/__init__.py | 2 +- merlin/spec/all_keys.py | 2 +- merlin/spec/defaults.py | 2 +- merlin/spec/expansion.py | 2 +- merlin/spec/override.py | 2 +- merlin/spec/specification.py | 2 +- merlin/study/__init__.py | 2 +- merlin/study/batch.py | 2 +- merlin/study/celeryadapter.py | 2 +- merlin/study/dag.py | 2 +- merlin/study/script_adapter.py | 2 +- merlin/study/step.py | 2 +- merlin/study/study.py | 2 +- merlin/utils.py | 2 +- setup.py | 2 +- tests/integration/conditions.py | 2 +- tests/integration/run_tests.py | 2 +- tests/integration/test_definitions.py | 2 +- 55 files changed, 56 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e60ea9d..c0760da46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,7 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] -### Fixed - +## [1.10.3] ### Added - The *.conf regex for the recursive-include of the merlin server directory so that pip will add it to the wheel - A note to the docs for how to fix an issue where the `merlin server start` command hangs diff --git a/Makefile b/Makefile index 030ed8d15..74c407db0 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/__init__.py b/merlin/__init__.py index 9856d9a7b..12ba225cd 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # @@ -38,7 +38,7 @@ import sys -__version__ = "1.10.2" +__version__ = "1.10.3" VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/ascii_art.py b/merlin/ascii_art.py index 5d61d8ed1..b56da4d7a 100644 --- a/merlin/ascii_art.py +++ b/merlin/ascii_art.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/celery.py b/merlin/celery.py index dc0cfd9d7..9921bbb89 100644 --- a/merlin/celery.py +++ b/merlin/celery.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/__init__.py b/merlin/common/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/common/__init__.py +++ b/merlin/common/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/__init__.py b/merlin/common/abstracts/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/common/abstracts/__init__.py +++ b/merlin/common/abstracts/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/enums/__init__.py b/merlin/common/abstracts/enums/__init__.py index be663a572..a90133b73 100644 --- a/merlin/common/abstracts/enums/__init__.py +++ b/merlin/common/abstracts/enums/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/openfilelist.py b/merlin/common/openfilelist.py index a4c127a12..00aaea917 100644 --- a/merlin/common/openfilelist.py +++ b/merlin/common/openfilelist.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/opennpylib.py b/merlin/common/opennpylib.py index f8b881dcf..0f7607a8e 100644 --- a/merlin/common/opennpylib.py +++ b/merlin/common/opennpylib.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index.py b/merlin/common/sample_index.py index 821c1df71..4f7333f6d 100644 --- a/merlin/common/sample_index.py +++ b/merlin/common/sample_index.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index_factory.py b/merlin/common/sample_index_factory.py index fe9d735d4..55601073e 100644 --- a/merlin/common/sample_index_factory.py +++ b/merlin/common/sample_index_factory.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/__init__.py b/merlin/common/security/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/common/security/__init__.py +++ b/merlin/common/security/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt.py b/merlin/common/security/encrypt.py index dfd230a1a..1059383d9 100644 --- a/merlin/common/security/encrypt.py +++ b/merlin/common/security/encrypt.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt_backend_traffic.py b/merlin/common/security/encrypt_backend_traffic.py index 2c70fadc9..cee757b91 100644 --- a/merlin/common/security/encrypt_backend_traffic.py +++ b/merlin/common/security/encrypt_backend_traffic.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index b24e0af3e..56051756b 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/util_sampling.py b/merlin/common/util_sampling.py index 1f851abb2..8137e0543 100644 --- a/merlin/common/util_sampling.py +++ b/merlin/common/util_sampling.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/__init__.py b/merlin/config/__init__.py index a53190df7..7af320b52 100644 --- a/merlin/config/__init__.py +++ b/merlin/config/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/broker.py b/merlin/config/broker.py index 8c7dc8a2b..78658333a 100644 --- a/merlin/config/broker.py +++ b/merlin/config/broker.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/celeryconfig.py b/merlin/config/celeryconfig.py index 73605768e..e688945cc 100644 --- a/merlin/config/celeryconfig.py +++ b/merlin/config/celeryconfig.py @@ -10,7 +10,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/configfile.py b/merlin/config/configfile.py index 6ccf3dea5..d46a1d038 100644 --- a/merlin/config/configfile.py +++ b/merlin/config/configfile.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index d30d5ab58..a619ecb03 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/utils.py b/merlin/config/utils.py index 2f5434091..1385c4f35 100644 --- a/merlin/config/utils.py +++ b/merlin/config/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/data/celery/__init__.py b/merlin/data/celery/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/data/celery/__init__.py +++ b/merlin/data/celery/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/display.py b/merlin/display.py index bf60bb50d..f59255ddb 100644 --- a/merlin/display.py +++ b/merlin/display.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/__init__.py b/merlin/examples/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/examples/__init__.py +++ b/merlin/examples/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/examples.py b/merlin/examples/examples.py index a7754cec7..78152e6ee 100644 --- a/merlin/examples/examples.py +++ b/merlin/examples/examples.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index 5b893ab9e..5dbe2ebf5 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index b176378a9..ed7156aa5 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/log_formatter.py b/merlin/log_formatter.py index e90e13324..6a6da63d8 100644 --- a/merlin/log_formatter.py +++ b/merlin/log_formatter.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/main.py b/merlin/main.py index d3fe4b80a..a29546798 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/merlin_templates.py b/merlin/merlin_templates.py index b2bcc4949..477887794 100644 --- a/merlin/merlin_templates.py +++ b/merlin/merlin_templates.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/router.py b/merlin/router.py index 779333dc6..465f0ad3d 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/__init__.py b/merlin/server/__init__.py index 6cd46fa97..88f37fd2c 100644 --- a/merlin/server/__init__.py +++ b/merlin/server/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index 93f928f3b..45e6ef3d3 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_config.py b/merlin/server/server_config.py index e088eedcd..5f109f7c8 100644 --- a/merlin/server/server_config.py +++ b/merlin/server/server_config.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_util.py b/merlin/server/server_util.py index a3cc7f021..a280abac5 100644 --- a/merlin/server/server_util.py +++ b/merlin/server/server_util.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/__init__.py b/merlin/spec/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/spec/__init__.py +++ b/merlin/spec/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/all_keys.py b/merlin/spec/all_keys.py index 145eb00bb..7f9f66188 100644 --- a/merlin/spec/all_keys.py +++ b/merlin/spec/all_keys.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index 1560f412d..c4aad952c 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/expansion.py b/merlin/spec/expansion.py index abba6ef02..e29f0e7b5 100644 --- a/merlin/spec/expansion.py +++ b/merlin/spec/expansion.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/override.py b/merlin/spec/override.py index e9ec4bceb..50d4f1c35 100644 --- a/merlin/spec/override.py +++ b/merlin/spec/override.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 1cb26d512..eb165b617 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/__init__.py b/merlin/study/__init__.py index 05c5d0bbf..2a6208883 100644 --- a/merlin/study/__init__.py +++ b/merlin/study/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/batch.py b/merlin/study/batch.py index b2157ebde..4298fca32 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index f9c04b1e3..31ef03b7c 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/dag.py b/merlin/study/dag.py index d2315bf7f..6c977a756 100644 --- a/merlin/study/dag.py +++ b/merlin/study/dag.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index dc01ad6a9..be66e1b97 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/step.py b/merlin/study/step.py index 686b5afa2..bdba0250d 100644 --- a/merlin/study/step.py +++ b/merlin/study/step.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/study.py b/merlin/study/study.py index 8eaf306ca..8f4ddb19d 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/utils.py b/merlin/utils.py index a2e4966d0..3eb4e5acc 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/setup.py b/setup.py index 0bb413325..d409c0a74 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index 0bc687923..81c063112 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index 82a4fdd92..bc19fd9c8 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index e9e9ce590..0fdedf07b 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.2. +# This file is part of Merlin, Version: 1.10.3. # # For details, see https://github.com/LLNL/merlin. # From d8bfbdbd5a07a314064ce24a6201662a34fc8028 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:54:18 -0700 Subject: [PATCH 16/48] bugfix/sphinx-5.3.0-requirement (#446) * Version/1.10.3 (#445) * fix default worker bug with all steps * version bump and requirements fix * Bugfix/filename-special-vars (#425) * fix file naming bug * fix filename bug with variable as study name * add tests for the file name special vars changes * modify changelog * implement Luc's suggestions * remove replace line * Create dependabot-changelog-updater.yml * testing outputs of modifying changelog * delete dependabot-changelog-updater * feature/pdf-docs (#427) * first attempt at adding pdf * fixing build error * modify changelog to show docs changes * fix errors Luc found in the build logs * trying out removal of latex * reverting latex changes back * uncommenting the latex_elements settings * adding epub to see if latex will build * adding a latex engine variable to conf * fix naming error with latex_engine * attempting to add a logo to the pdf build * testing an override to the searchtools file * revert back to not using searchtools override * update changelog * bugfix/openfoam_singularity_issues (#426) * fix openfoam_singularity issues * update requirements and descriptions for openfoam examples * bugfix/output-path-substitution (#430) * fix bug with output_path and variable substitution * add tests for cli substitutions * bugfix/scheduler-permission-error (#436) * Release/1.10.2 (#437) * bump version to 1.10.2 * bump version in CHANGELOG * resolve develop to main merge issues (#439) * fix default worker bug with all steps * version bump and requirements fix * dependabot/certifi-requests-pygments (#441) * Bump certifi from 2022.12.7 to 2023.7.22 in /docs Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] * add all dependabot changes and update CHANGELOG --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * bugfix/server-pip-redis-conf (#443) * add *.conf to the MANIFEST file so pip will grab the redis.conf file * add note explaining how to fix a hanging merlin server start * modify CHANGELOG * add second export option to docs and fix typo * bump to version 1.10.3 (#444) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * change hardcoded sphinx requirement * update CHANGELOG --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 +++ docs/requirements.in | 4 +++ docs/requirements.txt | 57 +------------------------------------------ 3 files changed, 9 insertions(+), 56 deletions(-) create mode 100644 docs/requirements.in diff --git a/CHANGELOG.md b/CHANGELOG.md index c0760da46..beecf82f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] +### Changed +- Hardcoded Sphinx v5.3.0 requirement is now removed so we can use latest Sphinx + ## [1.10.3] ### Added - The *.conf regex for the recursive-include of the merlin server directory so that pip will add it to the wheel diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 000000000..268785121 --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,4 @@ +# This file will list all requirements for the docs so we can freeze a version of them for release. +# To freeze the versions run: +# pip-compile requirements.in +sphinx \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index c771e60dc..5d3faecfe 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,56 +1 @@ -# -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: -# -# pip-compile requirements.in -# -alabaster==0.7.12 - # via sphinx -babel==2.10.3 - # via sphinx -certifi==2023.7.22 - # via requests -charset-normalizer==2.1.1 - # via requests -docutils==0.17.1 - # via sphinx -idna==3.4 - # via requests -imagesize==1.4.1 - # via sphinx -importlib-metadata==5.0.0 - # via sphinx -jinja2==3.0.3 - # via sphinx -markupsafe==2.1.1 - # via jinja2 -packaging==21.3 - # via sphinx -pygments==2.15.0 - # via sphinx -pyparsing==3.0.9 - # via packaging -pytz==2022.5 - # via babel -requests==2.31.0 - # via sphinx -snowballstemmer==2.2.0 - # via sphinx -sphinx==5.3.0 - # via -r requirements.in -sphinxcontrib-applehelp==1.0.2 - # via sphinx -sphinxcontrib-devhelp==1.0.2 - # via sphinx -sphinxcontrib-htmlhelp==2.0.0 - # via sphinx -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==1.0.3 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 - # via sphinx -urllib3==1.26.12 - # via requests -zipp==3.10.0 - # via importlib-metadata +sphinx>=5.3.0 From ea715db3db6b98215dbfb9499ec565e966a2056e Mon Sep 17 00:00:00 2001 From: Brian Gunnarson Date: Thu, 24 Aug 2023 18:21:09 -0700 Subject: [PATCH 17/48] remove github text that was causing errors --- merlin/__init__.py | 9 +-------- merlin/config/results_backend.py | 4 ---- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/merlin/__init__.py b/merlin/__init__.py index b3219ad1b..648299b35 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -38,15 +38,8 @@ import sys -<<<<<<< HEAD -<<<<<<< HEAD -__version__ = "1.10.1" -======= __version__ = "1.10.3" ->>>>>>> faf71edb866548cf686c2cef0fee559e01db28ce -======= -__version__ = "1.10.2" ->>>>>>> 77d09c3fe40df699e2f37701c07f28ff40c84b65 + VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index 257048aef..a619ecb03 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,11 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -<<<<<<< HEAD # This file is part of Merlin, Version: 1.10.3. -======= -# This file is part of Merlin, Version: 1.10.2. ->>>>>>> 77d09c3fe40df699e2f37701c07f28ff40c84b65 # # For details, see https://github.com/LLNL/merlin. # From 8241bfe21d5eb58fd2f9f8a3235181c0ea27faaf Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:19:59 -0700 Subject: [PATCH 18/48] feature/vlauncher (#447) * fix file naming error for iterative workflows * fixed small bug with new filepath naming * add VLAUNCHER functionality * add docs for VLAUNCHER and modify changelog * re-word docs and fix table format * add a test for vlauncher * run fix-style and add a test for vlauncher * Add the find_vlaunch_var and setup_vlaunch functions. The numeric value of the shell variables may not be defined until run time, so replace with variable strings instead of values. Consolidate the commands into one function. * Add variable set for (t)csh. * Run fix-style * make step settings the defaults and ignore commented lines * add some additional tests * remove regex library import --------- Co-authored-by: Joseph M. Koning --- CHANGELOG.md | 7 ++ docs/source/merlin_variables.rst | 53 ++++++++++++- merlin/spec/defaults.py | 8 ++ merlin/spec/specification.py | 13 +++- merlin/study/script_adapter.py | 77 ++++++++++++++++++- merlin/study/study.py | 3 +- merlin/utils.py | 20 +++++ tests/integration/test_definitions.py | 82 +++++++++++++++++++-- tests/integration/test_specs/flux_test.yaml | 28 +++++++ 9 files changed, 279 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beecf82f7..135508a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +### Added +- New reserved variables: + - `VLAUNCHER`: The same functionality as the `LAUNCHER` variable, but will substitute shell variables `MERLIN_NODES`, `MERLIN_PROCS`, `MERLIN_CORES`, and `MERLIN_GPUS` for nodes, procs, cores per task, and gpus + ### Changed - Hardcoded Sphinx v5.3.0 requirement is now removed so we can use latest Sphinx +### Fixed +- A bug where the filenames in iterative workflows kept appending `.out`, `.partial`, or `.expanded` to the filenames stored in the `merlin_info/` subdirectory + ## [1.10.3] ### Added - The *.conf regex for the recursive-include of the merlin server directory so that pip will add it to the wheel diff --git a/docs/source/merlin_variables.rst b/docs/source/merlin_variables.rst index d67e06412..f8ea7fce7 100644 --- a/docs/source/merlin_variables.rst +++ b/docs/source/merlin_variables.rst @@ -160,8 +160,9 @@ Reserved variables $(MERLIN_INFO)/*.expanded.yaml -The ``LAUNCHER`` Variable -+++++++++++++++++++++++++ + +The ``LAUNCHER`` and ``VLAUNCHER`` Variables ++++++++++++++++++++++++++++++++++++++++++++++++ ``$(LAUNCHER)`` is a special case of a reserved variable since it's value *can* be changed. It serves as an abstraction to launch a job with parallel schedulers like :ref:`slurm`, @@ -187,6 +188,54 @@ We can modify this to use the ``$(LAUNCHER)`` variable like so: In other words, the ``$(LAUNCHER)`` variable would become ``srun -N 1 -n 3``. +Similarly, the ``$(VLAUNCHER)`` variable behaves similarly to the ``$(LAUNCHER)`` variable. +The key distinction lies in its source of information. Instead of drawing certain configuration +options from the ``run`` section of a step, it retrieves specific shell variables. These shell +variables are automatically generated by Merlin when you include the ``$(VLAUNCHER)`` variable +in a step command, but they can also be customized by the user. Currently, the following shell +variables are: + +.. list-table:: VLAUNCHER Variables + :widths: 25 50 25 + :header-rows: 1 + + * - Variable + - Description + - Default + + * - ``${MERLIN_NODES}`` + - The number of nodes + - 1 + + * - ``${MERLIN_PROCS}`` + - The number of tasks/procs + - 1 + + * - ``${MERLIN_CORES}`` + - The number of cores per task/proc + - 1 + + * - ``${MERLIN_GPUS}`` + - The number of gpus per task/proc + - 0 + +Let's say we have the following defined in our yaml file: + +.. code:: yaml + + batch: + type: flux + + run: + cmd: | + MERLIN_NODES=4 + MERLIN_PROCS=2 + MERLIN_CORES=8 + MERLIN_GPUS=2 + $(VLAUNCHER) python script.py + +The ``$(VLAUNCHER)`` variable would be substituted to ``flux run -N 4 -n 2 -c 8 -g 2``. + User variables ------------------- Variables defined by a specification file in the ``env`` section, as in this example: diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index c4aad952c..6845b8b08 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -52,3 +52,11 @@ "generate": {"cmd": "echo 'Insert sample-generating command here'"}, "level_max_dirs": 25, } + +# Values of the form (step key to search for, default value if no step key found) +VLAUNCHER_VARS = { + "MERLIN_NODES": ("nodes", 1), + "MERLIN_PROCS": ("procs", 1), + "MERLIN_CORES": ("cores per task", 1), + "MERLIN_GPUS": ("gpus", 0), +} diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index eb165b617..60a47964d 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -45,7 +45,7 @@ from maestrowf.specification import YAMLSpecification from merlin.spec import all_keys, defaults -from merlin.utils import repr_timedelta +from merlin.utils import find_vlaunch_var, repr_timedelta LOG = logging.getLogger(__name__) @@ -369,6 +369,17 @@ def process_spec_defaults(self): defaults.STUDY_STEP_RUN["shell"] = self.batch["shell"] for step in self.study: MerlinSpec.fill_missing_defaults(step["run"], defaults.STUDY_STEP_RUN) + # Insert VLAUNCHER specific variables if necessary + if "$(VLAUNCHER)" in step["run"]["cmd"]: + SHSET = "" + if "csh" in step["run"]["shell"]: + SHSET = "set " + # We need to set default values for VLAUNCHER variables if they're not defined by the user + for vlaunch_var, vlaunch_val in defaults.VLAUNCHER_VARS.items(): + if not find_vlaunch_var(vlaunch_var.replace("MERLIN_", ""), step["run"]["cmd"], accept_no_matches=True): + # Look for predefined nodes/procs/cores/gpus values in the step and default to those + vlaunch_val = step["run"][vlaunch_val[0]] if vlaunch_val[0] in step["run"] else vlaunch_val[1] + step["run"]["cmd"] = f"{SHSET}{vlaunch_var}={vlaunch_val}\n" + step["run"]["cmd"] # fill in missing merlin section defaults MerlinSpec.fill_missing_defaults(self.merlin, defaults.MERLIN["merlin"]) diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index be66e1b97..80e56f279 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -42,12 +42,36 @@ from maestrowf.utils import start_process from merlin.common.abstracts.enums import ReturnCode -from merlin.utils import convert_timestring +from merlin.utils import convert_timestring, find_vlaunch_var LOG = logging.getLogger(__name__) +def setup_vlaunch(step_run: str, batch_type: str, gpu_config: bool) -> None: + """ + Check for the VLAUNCHER keyword int the step run string, find + the MERLIN variables and configure VLAUNCHER. + + :param `step_run`: the step.run command string + :param `batch_type`: the batch type string + :param `gpu_config`: bool to determin if gpus should be configured + :returns: None + """ + if "$(VLAUNCHER)" in step_run["cmd"]: + step_run["cmd"] = step_run["cmd"].replace("$(VLAUNCHER)", "$(LAUNCHER)") + + step_run["nodes"] = find_vlaunch_var("NODES", step_run["cmd"]) + step_run["procs"] = find_vlaunch_var("PROCS", step_run["cmd"]) + step_run["cores per task"] = find_vlaunch_var("CORES", step_run["cmd"]) + + if find_vlaunch_var("GPUS", step_run["cmd"]): + if gpu_config: + step_run["gpus"] = find_vlaunch_var("GPUS", step_run["cmd"]) + else: + LOG.warning(f"Merlin does not yet have the ability to set GPUs per task with {batch_type}. Coming soon.") + + class MerlinLSFScriptAdapter(SlurmScriptAdapter): """ A SchedulerScriptAdapter class for slurm blocking parallel launches, @@ -156,6 +180,23 @@ def get_parallelize_command(self, procs, nodes=None, **kwargs): return " ".join(args) + def write_script(self, ws_path, step): + """ + This will overwrite the write_script in method from Maestro's base ScriptAdapter + class but will eventually call it. This is necessary for the VLAUNCHER to work. + + :param `ws_path`: the path to the workspace where we'll write the scripts + :param `step`: the Maestro StudyStep object containing info for our step + :returns: a tuple containing: + - a boolean representing whether this step is to be scheduled or not + - Merlin can ignore this + - a path to the script for the cmd + - a path to the script for the restart cmd + """ + setup_vlaunch(step.run, "lsf", False) + + return super().write_script(ws_path, step) + class MerlinSlurmScriptAdapter(SlurmScriptAdapter): """ @@ -256,6 +297,23 @@ def get_parallelize_command(self, procs, nodes=None, **kwargs): return " ".join(args) + def write_script(self, ws_path, step): + """ + This will overwrite the write_script in method from Maestro's base ScriptAdapter + class but will eventually call it. This is necessary for the VLAUNCHER to work. + + :param `ws_path`: the path to the workspace where we'll write the scripts + :param `step`: the Maestro StudyStep object containing info for our step + :returns: a tuple containing: + - a boolean representing whether this step is to be scheduled or not + - Merlin can ignore this + - a path to the script for the cmd + - a path to the script for the restart cmd + """ + setup_vlaunch(step.run, "slurm", False) + + return super().write_script(ws_path, step) + class MerlinFluxScriptAdapter(MerlinSlurmScriptAdapter): """ @@ -319,6 +377,23 @@ def time_format(self, val): """ return convert_timestring(val, format_method="FSD") + def write_script(self, ws_path, step): + """ + This will overwrite the write_script in method from Maestro's base ScriptAdapter + class but will eventually call it. This is necessary for the VLAUNCHER to work. + + :param `ws_path`: the path to the workspace where we'll write the scripts + :param `step`: the Maestro StudyStep object containing info for our step + :returns: a tuple containing: + - a boolean representing whether this step is to be scheduled or not + - Merlin can ignore this + - a path to the script for the cmd + - a path to the script for the restart cmd + """ + setup_vlaunch(step.run, "flux", True) + + return super().write_script(ws_path, step) + class MerlinScriptAdapter(LocalScriptAdapter): """ diff --git a/merlin/study/study.py b/merlin/study/study.py index 8f4ddb19d..831062e17 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -127,7 +127,8 @@ def __init__( # pylint: disable=R0913 def _set_special_file_vars(self): """Setter for the orig, partial, and expanded file paths of a study.""" - base_name = Path(self.filepath).stem + shortened_filepath = self.filepath.replace(".out", "").replace(".partial", "").replace(".expanded", "") + base_name = Path(shortened_filepath).stem self.special_vars["MERLIN_SPEC_ORIGINAL_TEMPLATE"] = os.path.join( self.info, base_name + ".orig.yaml", diff --git a/merlin/utils.py b/merlin/utils.py index 3eb4e5acc..51a1fd8c0 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -497,6 +497,26 @@ def contains_shell_ref(string): return False +def find_vlaunch_var(vlaunch_var: str, step_cmd: str, accept_no_matches=False) -> str: + """ + Given a variable used for VLAUNCHER and the step cmd value, find + the variable. + + :param `vlaunch_var`: The name of the VLAUNCHER variable (without MERLIN_) + :param `step_cmd`: The string for the cmd of a step + :param `accept_no_matches`: If True, return None if we couldn't find the variable. Otherwise, raise an error. + :returns: the `vlaunch_var` variable or None + """ + matches = list(re.findall(rf"^(?!#).*MERLIN_{vlaunch_var}=\d+", step_cmd, re.MULTILINE)) + + if matches: + return f"${{MERLIN_{vlaunch_var}}}" + + if accept_no_matches: + return None + raise ValueError(f"VLAUNCHER used but could not find MERLIN_{vlaunch_var} in the step.") + + # Time utilities def convert_to_timedelta(timestr: Union[str, int]) -> timedelta: """Convert a timestring to a timedelta object. diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index 0fdedf07b..2f37dcc95 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -407,13 +407,81 @@ def define_tests(): # pylint: disable=R0914,R0915 }, "dry launch flux": { "cmds": f"{run} {flux} --dry --local --no-errors --vars N_SAMPLES=2 OUTPUT_PATH=./{OUTPUT_DIR}", - "conditions": StepFileHasRegex( - "runs", - "*/runs.slurm.sh", - "flux_test", - OUTPUT_DIR, - get_flux_cmd("flux", no_errors=True), - ), + "conditions": [ + StepFileHasRegex( + "runs", + "*/runs.slurm.sh", + "flux_test", + OUTPUT_DIR, + get_flux_cmd("flux", no_errors=True), + ), + ################## + # VLAUNCHER TESTS + ################## + StepFileHasRegex( + "vlauncher_test", + "vlauncher_test.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"flux run -n \$\{MERLIN_PROCS\} -N \$\{MERLIN_NODES\} -c \$\{MERLIN_CORES\}", + ), + StepFileHasRegex( + "vlauncher_test_step_defaults", + "vlauncher_test_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_GPUS=1", + ), + StepFileHasRegex( + "vlauncher_test_step_defaults", + "vlauncher_test_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_NODES=6", + ), + StepFileHasRegex( + "vlauncher_test_step_defaults", + "vlauncher_test_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_PROCS=3", + ), + StepFileHasRegex( + "vlauncher_test_step_defaults", + "vlauncher_test_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_CORES=2", + ), + StepFileHasRegex( + "vlauncher_test_no_step_defaults", + "vlauncher_test_no_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_GPUS=0", + ), + StepFileHasRegex( + "vlauncher_test_no_step_defaults", + "vlauncher_test_no_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_NODES=1", + ), + StepFileHasRegex( + "vlauncher_test_no_step_defaults", + "vlauncher_test_no_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_PROCS=1", + ), + StepFileHasRegex( + "vlauncher_test_no_step_defaults", + "vlauncher_test_no_step_defaults.slurm.sh", + "flux_test", + OUTPUT_DIR, + r"MERLIN_CORES=1", + ), + ], "run type": "local", }, "dry launch lsf": { diff --git a/tests/integration/test_specs/flux_test.yaml b/tests/integration/test_specs/flux_test.yaml index fe0130526..99f15205c 100644 --- a/tests/integration/test_specs/flux_test.yaml +++ b/tests/integration/test_specs/flux_test.yaml @@ -33,6 +33,34 @@ study: depends: [runs*] task_queue: flux_test +- description: step that uses vlauncher + name: vlauncher_test + run: + cmd: | + MERLIN_NODES=6 + MERLIN_PROCS=3 + MERLIN_CORES=2 + $(VLAUNCHER) echo "step that uses vlauncher" + task_queue: flux_test + +- description: test vlauncher step defaults + name: vlauncher_test_step_defaults + run: + cmd: | + $(VLAUNCHER) echo "test vlauncher step defaults" + task_queue: flux_test + nodes: 6 + procs: 3 + cores per task: 2 + gpus: 1 + +- description: test vlauncher no step defaults + name: vlauncher_test_no_step_defaults + run: + cmd: | + $(VLAUNCHER) echo "test vlauncher no step defaults" + task_queue: flux_test + global.parameters: STUDY: label: STUDY.%% From 50d0fb6a906146cf40fe06fb15edf0f85f87670b Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Thu, 28 Sep 2023 08:14:38 -0700 Subject: [PATCH 19/48] release/1.11.0 (#448) --- CHANGELOG.md | 4 ++-- Makefile | 2 +- merlin/__init__.py | 4 ++-- merlin/ascii_art.py | 2 +- merlin/celery.py | 2 +- merlin/common/__init__.py | 2 +- merlin/common/abstracts/__init__.py | 2 +- merlin/common/abstracts/enums/__init__.py | 2 +- merlin/common/openfilelist.py | 2 +- merlin/common/opennpylib.py | 2 +- merlin/common/sample_index.py | 2 +- merlin/common/sample_index_factory.py | 2 +- merlin/common/security/__init__.py | 2 +- merlin/common/security/encrypt.py | 2 +- merlin/common/security/encrypt_backend_traffic.py | 2 +- merlin/common/tasks.py | 2 +- merlin/common/util_sampling.py | 2 +- merlin/config/__init__.py | 2 +- merlin/config/broker.py | 2 +- merlin/config/celeryconfig.py | 2 +- merlin/config/configfile.py | 2 +- merlin/config/results_backend.py | 2 +- merlin/config/utils.py | 2 +- merlin/data/celery/__init__.py | 2 +- merlin/display.py | 2 +- merlin/examples/__init__.py | 2 +- merlin/examples/examples.py | 2 +- merlin/examples/generator.py | 2 +- merlin/exceptions/__init__.py | 2 +- merlin/log_formatter.py | 2 +- merlin/main.py | 2 +- merlin/merlin_templates.py | 2 +- merlin/router.py | 2 +- merlin/server/__init__.py | 2 +- merlin/server/server_commands.py | 2 +- merlin/server/server_config.py | 2 +- merlin/server/server_util.py | 2 +- merlin/spec/__init__.py | 2 +- merlin/spec/all_keys.py | 2 +- merlin/spec/defaults.py | 2 +- merlin/spec/expansion.py | 2 +- merlin/spec/override.py | 2 +- merlin/spec/specification.py | 2 +- merlin/study/__init__.py | 2 +- merlin/study/batch.py | 2 +- merlin/study/celeryadapter.py | 2 +- merlin/study/dag.py | 2 +- merlin/study/script_adapter.py | 2 +- merlin/study/step.py | 2 +- merlin/study/study.py | 2 +- merlin/utils.py | 2 +- setup.py | 2 +- tests/integration/conditions.py | 2 +- tests/integration/run_tests.py | 2 +- tests/integration/test_definitions.py | 2 +- 55 files changed, 57 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 135508a8c..914d8616a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] +## [1.11.0] ### Added -- New reserved variables: +- New reserved variable: - `VLAUNCHER`: The same functionality as the `LAUNCHER` variable, but will substitute shell variables `MERLIN_NODES`, `MERLIN_PROCS`, `MERLIN_CORES`, and `MERLIN_GPUS` for nodes, procs, cores per task, and gpus ### Changed diff --git a/Makefile b/Makefile index 74c407db0..4a857a217 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/__init__.py b/merlin/__init__.py index 12ba225cd..20a0e8b3e 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # @@ -38,7 +38,7 @@ import sys -__version__ = "1.10.3" +__version__ = "1.11.0" VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/ascii_art.py b/merlin/ascii_art.py index b56da4d7a..f823937a6 100644 --- a/merlin/ascii_art.py +++ b/merlin/ascii_art.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/celery.py b/merlin/celery.py index 9921bbb89..95f26530e 100644 --- a/merlin/celery.py +++ b/merlin/celery.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/__init__.py b/merlin/common/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/common/__init__.py +++ b/merlin/common/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/__init__.py b/merlin/common/abstracts/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/common/abstracts/__init__.py +++ b/merlin/common/abstracts/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/enums/__init__.py b/merlin/common/abstracts/enums/__init__.py index a90133b73..7b8ab80f5 100644 --- a/merlin/common/abstracts/enums/__init__.py +++ b/merlin/common/abstracts/enums/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/openfilelist.py b/merlin/common/openfilelist.py index 00aaea917..124c7851d 100644 --- a/merlin/common/openfilelist.py +++ b/merlin/common/openfilelist.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/opennpylib.py b/merlin/common/opennpylib.py index 0f7607a8e..a8f8dffb2 100644 --- a/merlin/common/opennpylib.py +++ b/merlin/common/opennpylib.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index.py b/merlin/common/sample_index.py index 4f7333f6d..149d52e13 100644 --- a/merlin/common/sample_index.py +++ b/merlin/common/sample_index.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index_factory.py b/merlin/common/sample_index_factory.py index 55601073e..dc13d41d1 100644 --- a/merlin/common/sample_index_factory.py +++ b/merlin/common/sample_index_factory.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/__init__.py b/merlin/common/security/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/common/security/__init__.py +++ b/merlin/common/security/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt.py b/merlin/common/security/encrypt.py index 1059383d9..125ec5bed 100644 --- a/merlin/common/security/encrypt.py +++ b/merlin/common/security/encrypt.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt_backend_traffic.py b/merlin/common/security/encrypt_backend_traffic.py index cee757b91..68e178b77 100644 --- a/merlin/common/security/encrypt_backend_traffic.py +++ b/merlin/common/security/encrypt_backend_traffic.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index 56051756b..f1d06077a 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/util_sampling.py b/merlin/common/util_sampling.py index 8137e0543..c29763485 100644 --- a/merlin/common/util_sampling.py +++ b/merlin/common/util_sampling.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/__init__.py b/merlin/config/__init__.py index 7af320b52..0594ffe45 100644 --- a/merlin/config/__init__.py +++ b/merlin/config/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/broker.py b/merlin/config/broker.py index 78658333a..fe49ff162 100644 --- a/merlin/config/broker.py +++ b/merlin/config/broker.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/celeryconfig.py b/merlin/config/celeryconfig.py index e688945cc..0ff305962 100644 --- a/merlin/config/celeryconfig.py +++ b/merlin/config/celeryconfig.py @@ -10,7 +10,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/configfile.py b/merlin/config/configfile.py index d46a1d038..1f3418377 100644 --- a/merlin/config/configfile.py +++ b/merlin/config/configfile.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index a619ecb03..d3e7002e7 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/utils.py b/merlin/config/utils.py index 1385c4f35..65fc6f85c 100644 --- a/merlin/config/utils.py +++ b/merlin/config/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/data/celery/__init__.py b/merlin/data/celery/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/data/celery/__init__.py +++ b/merlin/data/celery/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/display.py b/merlin/display.py index f59255ddb..a0470938c 100644 --- a/merlin/display.py +++ b/merlin/display.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/__init__.py b/merlin/examples/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/examples/__init__.py +++ b/merlin/examples/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/examples.py b/merlin/examples/examples.py index 78152e6ee..1d756f00e 100644 --- a/merlin/examples/examples.py +++ b/merlin/examples/examples.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index 5dbe2ebf5..294787857 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index ed7156aa5..cf272d93b 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/log_formatter.py b/merlin/log_formatter.py index 6a6da63d8..3fba8cfc8 100644 --- a/merlin/log_formatter.py +++ b/merlin/log_formatter.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/main.py b/merlin/main.py index a29546798..198cf3804 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/merlin_templates.py b/merlin/merlin_templates.py index 477887794..7936db03b 100644 --- a/merlin/merlin_templates.py +++ b/merlin/merlin_templates.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/router.py b/merlin/router.py index 465f0ad3d..476ab1c0f 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/__init__.py b/merlin/server/__init__.py index 88f37fd2c..d04c75d72 100644 --- a/merlin/server/__init__.py +++ b/merlin/server/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index 45e6ef3d3..45411131b 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_config.py b/merlin/server/server_config.py index 5f109f7c8..414f7a407 100644 --- a/merlin/server/server_config.py +++ b/merlin/server/server_config.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_util.py b/merlin/server/server_util.py index a280abac5..2b8f1216d 100644 --- a/merlin/server/server_util.py +++ b/merlin/server/server_util.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/__init__.py b/merlin/spec/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/spec/__init__.py +++ b/merlin/spec/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/all_keys.py b/merlin/spec/all_keys.py index 7f9f66188..556f5924e 100644 --- a/merlin/spec/all_keys.py +++ b/merlin/spec/all_keys.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index 6845b8b08..8972d5cfe 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/expansion.py b/merlin/spec/expansion.py index e29f0e7b5..381bc72f4 100644 --- a/merlin/spec/expansion.py +++ b/merlin/spec/expansion.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/override.py b/merlin/spec/override.py index 50d4f1c35..f3192a38e 100644 --- a/merlin/spec/override.py +++ b/merlin/spec/override.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 60a47964d..32fe0f635 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/__init__.py b/merlin/study/__init__.py index 2a6208883..d6f53d03d 100644 --- a/merlin/study/__init__.py +++ b/merlin/study/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/batch.py b/merlin/study/batch.py index 4298fca32..e02a65a32 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index 31ef03b7c..8b5ff196d 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/dag.py b/merlin/study/dag.py index 6c977a756..ea4d22926 100644 --- a/merlin/study/dag.py +++ b/merlin/study/dag.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index 80e56f279..6ecc79c5f 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/step.py b/merlin/study/step.py index bdba0250d..5d877ba4f 100644 --- a/merlin/study/step.py +++ b/merlin/study/step.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/study.py b/merlin/study/study.py index 831062e17..b9ada35ea 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/utils.py b/merlin/utils.py index 51a1fd8c0..33735085d 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/setup.py b/setup.py index d409c0a74..7c91d26c7 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index 81c063112..b25010ca2 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index bc19fd9c8..58460e18f 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index 2f37dcc95..f59acf237 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.10.3. +# This file is part of Merlin, Version: 1.11.0. # # For details, see https://github.com/LLNL/merlin. # From 99d56598bcd2ed90743f5a0470f604fabfbc6704 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:46:23 -0700 Subject: [PATCH 20/48] bugfix/skewed-sample-hierarchy (#450) * add patch for skewed sample hierarchy/additional samples * update changelog * catch narrower range of exceptions --- CHANGELOG.md | 1 + merlin/common/tasks.py | 48 ++++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 914d8616a..f994a4d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - A bug where the filenames in iterative workflows kept appending `.out`, `.partial`, or `.expanded` to the filenames stored in the `merlin_info/` subdirectory +- A bug where a skewed sample hierarchy was created when a restart was necessary in the `add_merlin_expanded_chain_to_chord` task ## [1.10.3] ### Added diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index f1d06077a..fbd401826 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -298,27 +298,33 @@ def add_merlin_expanded_chain_to_chord( # pylint: disable=R0913,R0914 LOG.debug("chain added to chord") else: # recurse down the sample_index hierarchy - LOG.debug("recursing down sample_index hierarchy") - for next_index in sample_index.children.values(): - next_index.name = os.path.join(sample_index.name, next_index.name) - LOG.debug("generating next step") - next_step = add_merlin_expanded_chain_to_chord.s( - task_type, - chain_, - samples[next_index.min - min_sample_id : next_index.max - min_sample_id], - labels, - next_index, - adapter_config, - next_index.min, - ) - next_step.set(queue=chain_[0].get_task_queue()) - LOG.debug(f"recursing with range {next_index.min}:{next_index.max}, {next_index.name} {signature(next_step)}") - LOG.debug(f"queuing samples[{next_index.min}:{next_index.max}] in for {chain_} in {next_index.name}...") - if self.request.is_eager: - next_step.delay() - else: - self.add_to_chord(next_step, lazy=False) - LOG.debug(f"queued for samples[{next_index.min}:{next_index.max}] in for {chain_} in {next_index.name}") + try: + LOG.debug("recursing down sample_index hierarchy") + for next_index in sample_index.children.values(): + next_index_name_before = next_index.name + next_index.name = os.path.join(sample_index.name, next_index.name) + LOG.debug("generating next step") + next_step = add_merlin_expanded_chain_to_chord.s( + task_type, + chain_, + samples[next_index.min - min_sample_id : next_index.max - min_sample_id], + labels, + next_index, + adapter_config, + next_index.min, + ) + next_step.set(queue=chain_[0].get_task_queue()) + LOG.debug(f"recursing with range {next_index.min}:{next_index.max}, {next_index.name} {signature(next_step)}") + LOG.debug(f"queuing samples[{next_index.min}:{next_index.max}] in for {chain_} in {next_index.name}...") + if self.request.is_eager: + next_step.delay() + else: + self.add_to_chord(next_step, lazy=False) + LOG.debug(f"queued for samples[{next_index.min}:{next_index.max}] in for {chain_} in {next_index.name}") + except retry_exceptions as e: + # Reset the index to what it was before so we don't accidentally create a bunch of extra samples upon restart + next_index.name = next_index_name_before + raise e return ReturnCode.OK From 593dbcdf90096b722a97da8abe62e23c016f18e5 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:54:24 -0700 Subject: [PATCH 21/48] bugfix/lsf-gpu-typo (#453) * fix typo in batch.py that causes a bug * change print statements to log statements --- CHANGELOG.md | 4 ++++ merlin/study/batch.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f994a4d05..d709ccaa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +- Typo in `batch.py` that caused lsf launches to fail (`ALL_SGPUS` changed to `ALL_GPUS`) + ## [1.11.0] ### Added - New reserved variable: diff --git a/merlin/study/batch.py b/merlin/study/batch.py index e02a65a32..1b96cd282 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -299,7 +299,7 @@ def construct_scheduler_legend(parsed_batch: Dict, nodes: int) -> Dict: "lsf": { "check cmd": ["jsrun", "--help"], "expected check output": b"jsrun", - "launch": f"jsrun -a 1 -c ALL_CPUS -g ALL_SGPUS --bind=none -n {nodes}", + "launch": f"jsrun -a 1 -c ALL_CPUS -g ALL_GPUS --bind=none -n {nodes}", }, # pbs is mainly a placeholder in case a user wants to try it (we don't have it at the lab so it's mostly untested) "pbs": { @@ -335,12 +335,16 @@ def construct_worker_launch_command(parsed_batch: Dict, nodes: int) -> str: scheduler_legend: Dict = construct_scheduler_legend(parsed_batch, nodes) workload_manager: str = get_batch_type(scheduler_legend) + LOG.debug(f"parsed_batch: {parsed_batch}") + if parsed_batch["btype"] == "pbs" and workload_manager == parsed_batch["btype"]: raise TypeError("The PBS scheduler is only enabled for 'batch: flux' type") if parsed_batch["btype"] == "slurm" and workload_manager not in ("lsf", "flux", "pbs"): workload_manager = "slurm" + LOG.debug(f"workload_manager: {workload_manager}") + try: launch_command = scheduler_legend[workload_manager]["launch"] except KeyError as e: # pylint: disable=C0103 From f994f96f71d3405f59e108e0e67c9d2cd0ff3998 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:08:53 -0700 Subject: [PATCH 22/48] release/1.11.1 (#454) --- CHANGELOG.md | 2 +- Makefile | 2 +- merlin/__init__.py | 4 ++-- merlin/ascii_art.py | 2 +- merlin/celery.py | 2 +- merlin/common/__init__.py | 2 +- merlin/common/abstracts/__init__.py | 2 +- merlin/common/abstracts/enums/__init__.py | 2 +- merlin/common/openfilelist.py | 2 +- merlin/common/opennpylib.py | 2 +- merlin/common/sample_index.py | 2 +- merlin/common/sample_index_factory.py | 2 +- merlin/common/security/__init__.py | 2 +- merlin/common/security/encrypt.py | 2 +- merlin/common/security/encrypt_backend_traffic.py | 2 +- merlin/common/tasks.py | 2 +- merlin/common/util_sampling.py | 2 +- merlin/config/__init__.py | 2 +- merlin/config/broker.py | 2 +- merlin/config/celeryconfig.py | 2 +- merlin/config/configfile.py | 2 +- merlin/config/results_backend.py | 2 +- merlin/config/utils.py | 2 +- merlin/data/celery/__init__.py | 2 +- merlin/display.py | 2 +- merlin/examples/__init__.py | 2 +- merlin/examples/examples.py | 2 +- merlin/examples/generator.py | 2 +- merlin/exceptions/__init__.py | 2 +- merlin/log_formatter.py | 2 +- merlin/main.py | 2 +- merlin/merlin_templates.py | 2 +- merlin/router.py | 2 +- merlin/server/__init__.py | 2 +- merlin/server/server_commands.py | 2 +- merlin/server/server_config.py | 2 +- merlin/server/server_util.py | 2 +- merlin/spec/__init__.py | 2 +- merlin/spec/all_keys.py | 2 +- merlin/spec/defaults.py | 2 +- merlin/spec/expansion.py | 2 +- merlin/spec/override.py | 2 +- merlin/spec/specification.py | 2 +- merlin/study/__init__.py | 2 +- merlin/study/batch.py | 2 +- merlin/study/celeryadapter.py | 2 +- merlin/study/dag.py | 2 +- merlin/study/script_adapter.py | 2 +- merlin/study/step.py | 2 +- merlin/study/study.py | 2 +- merlin/utils.py | 2 +- setup.py | 2 +- tests/integration/conditions.py | 2 +- tests/integration/run_tests.py | 2 +- tests/integration/test_definitions.py | 2 +- 55 files changed, 56 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d709ccaa4..8d0ef2ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.11.1] ### Fixed - Typo in `batch.py` that caused lsf launches to fail (`ALL_SGPUS` changed to `ALL_GPUS`) diff --git a/Makefile b/Makefile index 4a857a217..2f9db031b 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/__init__.py b/merlin/__init__.py index 20a0e8b3e..c1ad21b22 100644 --- a/merlin/__init__.py +++ b/merlin/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # @@ -38,7 +38,7 @@ import sys -__version__ = "1.11.0" +__version__ = "1.11.1" VERSION = __version__ PATH_TO_PROJ = os.path.join(os.path.dirname(__file__), "") diff --git a/merlin/ascii_art.py b/merlin/ascii_art.py index f823937a6..3cca2c710 100644 --- a/merlin/ascii_art.py +++ b/merlin/ascii_art.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/celery.py b/merlin/celery.py index 95f26530e..55d616658 100644 --- a/merlin/celery.py +++ b/merlin/celery.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/__init__.py b/merlin/common/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/common/__init__.py +++ b/merlin/common/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/__init__.py b/merlin/common/abstracts/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/common/abstracts/__init__.py +++ b/merlin/common/abstracts/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/abstracts/enums/__init__.py b/merlin/common/abstracts/enums/__init__.py index 7b8ab80f5..383e7dccd 100644 --- a/merlin/common/abstracts/enums/__init__.py +++ b/merlin/common/abstracts/enums/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/openfilelist.py b/merlin/common/openfilelist.py index 124c7851d..d79e4e4f3 100644 --- a/merlin/common/openfilelist.py +++ b/merlin/common/openfilelist.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/opennpylib.py b/merlin/common/opennpylib.py index a8f8dffb2..da366b452 100644 --- a/merlin/common/opennpylib.py +++ b/merlin/common/opennpylib.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index.py b/merlin/common/sample_index.py index 149d52e13..4e3ac3a52 100644 --- a/merlin/common/sample_index.py +++ b/merlin/common/sample_index.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/sample_index_factory.py b/merlin/common/sample_index_factory.py index dc13d41d1..4303c3a6e 100644 --- a/merlin/common/sample_index_factory.py +++ b/merlin/common/sample_index_factory.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/__init__.py b/merlin/common/security/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/common/security/__init__.py +++ b/merlin/common/security/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt.py b/merlin/common/security/encrypt.py index 125ec5bed..806d42e0c 100644 --- a/merlin/common/security/encrypt.py +++ b/merlin/common/security/encrypt.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/security/encrypt_backend_traffic.py b/merlin/common/security/encrypt_backend_traffic.py index 68e178b77..e0957ebb8 100644 --- a/merlin/common/security/encrypt_backend_traffic.py +++ b/merlin/common/security/encrypt_backend_traffic.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index fbd401826..2bd77d2ad 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/common/util_sampling.py b/merlin/common/util_sampling.py index c29763485..134d0b66c 100644 --- a/merlin/common/util_sampling.py +++ b/merlin/common/util_sampling.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/__init__.py b/merlin/config/__init__.py index 0594ffe45..b58e3b2a9 100644 --- a/merlin/config/__init__.py +++ b/merlin/config/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/broker.py b/merlin/config/broker.py index fe49ff162..385b8c1df 100644 --- a/merlin/config/broker.py +++ b/merlin/config/broker.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/celeryconfig.py b/merlin/config/celeryconfig.py index 0ff305962..fbbd39064 100644 --- a/merlin/config/celeryconfig.py +++ b/merlin/config/celeryconfig.py @@ -10,7 +10,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/configfile.py b/merlin/config/configfile.py index 1f3418377..2ca6c5d04 100644 --- a/merlin/config/configfile.py +++ b/merlin/config/configfile.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/results_backend.py b/merlin/config/results_backend.py index d3e7002e7..b88655399 100644 --- a/merlin/config/results_backend.py +++ b/merlin/config/results_backend.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/config/utils.py b/merlin/config/utils.py index 65fc6f85c..8f0c6b029 100644 --- a/merlin/config/utils.py +++ b/merlin/config/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/data/celery/__init__.py b/merlin/data/celery/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/data/celery/__init__.py +++ b/merlin/data/celery/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/display.py b/merlin/display.py index a0470938c..78eee5866 100644 --- a/merlin/display.py +++ b/merlin/display.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/__init__.py b/merlin/examples/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/examples/__init__.py +++ b/merlin/examples/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/examples.py b/merlin/examples/examples.py index 1d756f00e..9b65f31ae 100644 --- a/merlin/examples/examples.py +++ b/merlin/examples/examples.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/examples/generator.py b/merlin/examples/generator.py index 294787857..2fa5e61ce 100644 --- a/merlin/examples/generator.py +++ b/merlin/examples/generator.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index cf272d93b..72d5a1521 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/log_formatter.py b/merlin/log_formatter.py index 3fba8cfc8..b8858f721 100644 --- a/merlin/log_formatter.py +++ b/merlin/log_formatter.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/main.py b/merlin/main.py index 198cf3804..55496a72c 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/merlin_templates.py b/merlin/merlin_templates.py index 7936db03b..a355c4f2f 100644 --- a/merlin/merlin_templates.py +++ b/merlin/merlin_templates.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/router.py b/merlin/router.py index 476ab1c0f..01a10aae7 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/__init__.py b/merlin/server/__init__.py index d04c75d72..7e2b6f1c1 100644 --- a/merlin/server/__init__.py +++ b/merlin/server/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index 45411131b..a28776577 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -8,7 +8,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_config.py b/merlin/server/server_config.py index 414f7a407..e4ec646fc 100644 --- a/merlin/server/server_config.py +++ b/merlin/server/server_config.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/server/server_util.py b/merlin/server/server_util.py index 2b8f1216d..bab641702 100644 --- a/merlin/server/server_util.py +++ b/merlin/server/server_util.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/__init__.py b/merlin/spec/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/spec/__init__.py +++ b/merlin/spec/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/all_keys.py b/merlin/spec/all_keys.py index 556f5924e..dcc02b063 100644 --- a/merlin/spec/all_keys.py +++ b/merlin/spec/all_keys.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/defaults.py b/merlin/spec/defaults.py index 8972d5cfe..32fd05aa5 100644 --- a/merlin/spec/defaults.py +++ b/merlin/spec/defaults.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/expansion.py b/merlin/spec/expansion.py index 381bc72f4..5924d1f74 100644 --- a/merlin/spec/expansion.py +++ b/merlin/spec/expansion.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/override.py b/merlin/spec/override.py index f3192a38e..abb0f13c9 100644 --- a/merlin/spec/override.py +++ b/merlin/spec/override.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/spec/specification.py b/merlin/spec/specification.py index 32fe0f635..ac23b06d1 100644 --- a/merlin/spec/specification.py +++ b/merlin/spec/specification.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/__init__.py b/merlin/study/__init__.py index d6f53d03d..e6dccdf56 100644 --- a/merlin/study/__init__.py +++ b/merlin/study/__init__.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/batch.py b/merlin/study/batch.py index 1b96cd282..01a2945e3 100644 --- a/merlin/study/batch.py +++ b/merlin/study/batch.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index 8b5ff196d..a6707d952 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/dag.py b/merlin/study/dag.py index ea4d22926..4cffc679c 100644 --- a/merlin/study/dag.py +++ b/merlin/study/dag.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/script_adapter.py b/merlin/study/script_adapter.py index 6ecc79c5f..45d211742 100644 --- a/merlin/study/script_adapter.py +++ b/merlin/study/script_adapter.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/step.py b/merlin/study/step.py index 5d877ba4f..c5773520a 100644 --- a/merlin/study/step.py +++ b/merlin/study/step.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/study/study.py b/merlin/study/study.py index b9ada35ea..6e2f4f937 100644 --- a/merlin/study/study.py +++ b/merlin/study/study.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/merlin/utils.py b/merlin/utils.py index 33735085d..196e8b29b 100644 --- a/merlin/utils.py +++ b/merlin/utils.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/setup.py b/setup.py index 7c91d26c7..7303a1ddf 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/conditions.py b/tests/integration/conditions.py index b25010ca2..80e3e5855 100644 --- a/tests/integration/conditions.py +++ b/tests/integration/conditions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index 58460e18f..c0b699055 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # diff --git a/tests/integration/test_definitions.py b/tests/integration/test_definitions.py index f59acf237..273fa7c56 100644 --- a/tests/integration/test_definitions.py +++ b/tests/integration/test_definitions.py @@ -6,7 +6,7 @@ # # LLNL-CODE-797170 # All rights reserved. -# This file is part of Merlin, Version: 1.11.0. +# This file is part of Merlin, Version: 1.11.1. # # For details, see https://github.com/LLNL/merlin. # From 5dc82061f235d1710518158b20819bc51ef022ce Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:45:48 -0700 Subject: [PATCH 23/48] Add Pytest Fixtures to Test Suite (#456) * begin work on integration refactor; create fixtures and initial tests * update CHANGELOG and run fix-style * add pytest fixtures and README explaining them * add tests to demonstrate how to use the fixtures * move/rename some files and modify integration's README * add password change to redis.pass file * fix lint issues * modify redis pwd for test server to be constant for each test * fix lint issue only caught on github ci --- CHANGELOG.md | 5 + merlin/common/tasks.py | 2 +- merlin/server/server_commands.py | 3 + merlin/study/celeryadapter.py | 34 +- requirements/dev.txt | 1 + tests/README.md | 152 +++++++++ tests/conftest.py | 301 ++++++++++++++++++ .../{test_definitions.py => definitions.py} | 0 tests/integration/run_tests.py | 6 +- tests/unit/study/test_celeryadapter.py | 160 ++++++++++ 10 files changed, 647 insertions(+), 17 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/conftest.py rename tests/integration/{test_definitions.py => definitions.py} (100%) create mode 100644 tests/unit/study/test_celeryadapter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0ef2ae6..9ca916ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Merlin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Pytest fixtures in the `conftest.py` file of the integration test suite +- Tests for the `celeryadapter.py` module + ## [1.11.1] ### Fixed - Typo in `batch.py` that caused lsf launches to fail (`ALL_SGPUS` changed to `ALL_GPUS`) diff --git a/merlin/common/tasks.py b/merlin/common/tasks.py index 2bd77d2ad..8292e559f 100644 --- a/merlin/common/tasks.py +++ b/merlin/common/tasks.py @@ -480,7 +480,7 @@ def expand_tasks_with_samples( # pylint: disable=R0913,R0914 if not found_tasks: for next_index_path, next_index in sample_index.traverse(conditional=condition): LOG.info( - f"generating next step for range {next_index.min}:{next_index.max} {next_index.max-next_index.min}" + f"generating next step for range {next_index.min}:{next_index.max} {next_index.max - next_index.min}" ) next_index.name = next_index_path diff --git a/merlin/server/server_commands.py b/merlin/server/server_commands.py index a28776577..c244c9eca 100644 --- a/merlin/server/server_commands.py +++ b/merlin/server/server_commands.py @@ -92,6 +92,9 @@ def config_server(args: Namespace) -> None: # pylint: disable=R0912 redis_users = RedisUsers(server_config.container.get_user_file_path()) redis_users.set_password("default", args.password) redis_users.write() + pass_file = server_config.container.get_pass_file_path() + with open(pass_file, "w") as pfile: + pfile.write(args.password) redis_config.set_directory(args.directory) diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index a6707d952..cd9714dff 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -37,6 +37,7 @@ import subprocess import time from contextlib import suppress +from typing import Dict, List, Optional from merlin.study.batch import batch_check_parallel, batch_worker_launch from merlin.utils import apply_list_of_regex, check_machines, get_procs, get_yaml_var, is_running @@ -69,23 +70,31 @@ def run_celery(study, run_mode=None): queue_merlin_study(study, adapter_config) -def get_running_queues(): +def get_running_queues(celery_app_name: str, test_mode: bool = False) -> List[str]: """ - Check for running celery workers with -Q queues - and return a unique list of the queues + Check for running celery workers by looking at the currently running processes. + If there are running celery workers, we'll pull the queues from the -Q tag in the + process command. The list returned here will contain only unique celery queue names. + This must be run on the allocation where the workers are running. - Must be run on the allocation where the workers are running + :param `celery_app_name`: The name of the celery app (typically merlin here unless testing) + :param `test_mode`: If True, run this function in test mode + :returns: A unique list of celery queues with workers attached to them """ running_queues = [] - if not is_running("celery worker"): + if not is_running(f"{celery_app_name} worker"): return running_queues - procs = get_procs("celery") + proc_name = "celery" if not test_mode else "sh" + procs = get_procs(proc_name) for _, lcmd in procs: lcmd = list(filter(None, lcmd)) cmdline = " ".join(lcmd) if "-Q" in cmdline: + if test_mode: + echo_cmd = lcmd.pop(2) + lcmd.extend(echo_cmd.split()) running_queues.extend(lcmd[lcmd.index("-Q") + 1].split(",")) running_queues = list(set(running_queues)) @@ -155,19 +164,20 @@ def get_active_workers(app): return worker_queue_map -def celerize_queues(queues): +def celerize_queues(queues: List[str], config: Optional[Dict] = None): """ Celery requires a queue tag to be prepended to their queues so this function will 'celerize' every queue in a list you provide it by prepending the queue tag. - :param `queues`: A list of queues that need the queue - tag prepended. + :param `queues`: A list of queues that need the queue tag prepended. + :param `config`: A dict of configuration settings """ - from merlin.config.configfile import CONFIG # pylint: disable=C0415 + if config is None: + from merlin.config.configfile import CONFIG as config # pylint: disable=C0415 for i, queue in enumerate(queues): - queues[i] = f"{CONFIG.celery.queue_tag}{queue}" + queues[i] = f"{config.celery.queue_tag}{queue}" def _build_output_table(worker_list, output_table): @@ -462,7 +472,7 @@ def start_celery_workers(spec, steps, celery_args, disable_logs, just_return_com running_queues.extend(local_queues) queues = queues.split(",") if not overlap: - running_queues.extend(get_running_queues()) + running_queues.extend(get_running_queues("merlin")) # Cache the queues from this worker to use to test # for existing queues in any subsequent workers. # If overlap is True, then do not check the local queues. diff --git a/requirements/dev.txt b/requirements/dev.txt index 9321694f8..895a89249 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,3 +10,4 @@ twine sphinx>=2.0.0 alabaster johnnydep +deepdiff diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..a6bf7005a --- /dev/null +++ b/tests/README.md @@ -0,0 +1,152 @@ +# Tests + +This directory utilizes pytest to create and run our test suite. +Here we use pytest fixtures to create a local redis server and a celery app for testing. + +This directory is organized like so: +- `conftest.py` - The script containing all fixtures for our tests +- `unit/` - The directory containing unit tests + - `test_*.py` - The actual test scripts to run +- `integration/` - The directory containing integration tests + + - `definitions.py` - The test definitions + - `run_tests.py` - The script to run the tests defined in `definitions.py` + - `conditions.py` - The conditions to test against + +## How to Run + +Before running any tests: + +1. Activate your virtual environment with Merlin's dev requirements installed +2. Navigate to the tests folder where this README is located + +To run the entire test suite: + +``` +python -m pytest +``` + +To run a specific test file: + +``` +python -m pytest /path/to/test_specific_file.py +``` + +To run a certain test class within a specific test file: + +``` +python -m pytest /path/to/test_specific_file.py::TestCertainClass +``` + +To run one unique test: + +``` +python -m pytest /path/to/test_specific_file.py::TestCertainClass::test_unique_test +``` + +## Killing the Test Server + +In case of an issue with the test suite, or if you stop the tests with `ctrl+C`, you may need to stop +the server manually. This can be done with: + +``` +redis-cli +127.0.0.1:6379> AUTH merlin-test-server +127.0.0.1:6379> shutdown +not connected> quit +``` + +## The Fixture Process Explained + +Pytest fixtures play a fundamental role in establishing a consistent foundation for test execution, +thus ensuring reliable and predictable test outcomes. This section will delve into essential aspects +of these fixtures, including how to integrate fixtures into tests, the utilization of fixtures within other fixtures, +their scope, and the yielding of fixture results. + +### How to Integrate Fixtures Into Tests + +Probably the most important part of fixtures is understanding how to use them. Luckily, this process is very +simple and can be dumbed down to 2 steps: + +1. Create a fixture in the `conftest.py` file by using the `@pytest.fixture` decorator. For example: + +``` +@pytest.fixture +def dummy_fixture(): + return "hello world" +``` + +2. Use it as an argument in a test function (you don't even need to import it!): + +``` +def test_dummy(dummy_fixture): + assert dummy_fixture == "hello world" +``` + +For more information, see [Pytest's documentation](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#how-to-use-fixtures). + +### Fixtureception + +One of the coolest and most useful aspects of fixtures that we utilize in this test suite is the ability for +fixtures to be used within other fixtures. For more info on this from pytest, see +[here](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#fixtures-can-request-other-fixtures). + +Pytest will handle fixtures within fixtures in a stack-based way. Let's look at how creating the `redis_pass` +fixture from our `conftest.py` file works in order to illustrate the process. +1. First, we start by telling pytest that we want to use the `redis_pass` fixture by providing it as an argument +to a test/fixture: + +``` +def test_example(redis_pass): + ... +``` + +2. Now pytest will find the `redis_pass` fixture and put it at the top of the stack to be created. However, +it'll see that this fixture requires another fixture `merlin_server_dir` as an argument: + +``` +@pytest.fixture(scope="session") +def redis_pass(merlin_server_dir): + ... +``` + +3. Pytest then puts the `merlin_server_dir` fixture at the top of the stack, but similarly it sees that this fixture +requires yet another fixture `temp_output_dir`: + +``` +@pytest.fixture(scope="session") +def merlin_server_dir(temp_output_dir: str) -> str: + ... +``` + +4. This process continues until it reaches a fixture that doesn't require any more fixtures. At this point the base +fixture is created and pytest will start working its way back up the stack to the first fixture it looked at (in this +case `redis_pass`). + +5. Once all required fixtures are created, execution will be returned to the test which can now access the fixture +that was requested (`redis_pass`). + +As you can see, if we have to re-do this process for every test it could get pretty time intensive. This is where fixture +scopes come to save the day. + +### Fixture Scopes + +There are several different scopes that you can set for fixtures. The majority of our fixtures use a `session` +scope so that we only have to create the fixtures one time (as some of them can take a few seconds to set up). +The goal is to create fixtures with the most general use-case in mind so that we can re-use them for larger +scopes, which helps with efficiency. + +For more info on scopes, see +[Pytest's Fixture Scope documentation](https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session). + +### Yielding Fixtures + +In several fixtures throughout our test suite, we need to run some sort of teardown for the fixture. For example, +once we no longer need the `redis_server` fixture, we need to shut the server down so it stops using resources. +This is where yielding fixtures becomes extremely useful. + +Using the `yield` keyword allows execution to be returned to a test that needs the fixture once the feature has +been set up. After all tests using the fixture have been ran, execution will return to the fixture for us to run +our teardown code. + +For more information on yielding fixtures, see [Pytest's documentation](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization). \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..a496175eb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,301 @@ +############################################################################### +# Copyright (c) 2023, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory +# Written by the Merlin dev team, listed in the CONTRIBUTORS file. +# +# +# LLNL-CODE-797170 +# All rights reserved. +# This file is part of Merlin, Version: 1.11.1. +# +# For details, see https://github.com/LLNL/merlin. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### +""" +This module contains pytest fixtures to be used throughout the entire +integration test suite. +""" +import multiprocessing +import os +import subprocess +from time import sleep +from typing import Dict, List + +import pytest +import redis +from _pytest.tmpdir import TempPathFactory +from celery import Celery + + +class RedisServerError(Exception): + """ + Exception to signal that the server wasn't pinged properly. + """ + + +class ServerInitError(Exception): + """ + Exception to signal that there was an error initializing the server. + """ + + +@pytest.fixture(scope="session") +def temp_output_dir(tmp_path_factory: TempPathFactory) -> str: + """ + This fixture will create a temporary directory to store output files of integration tests. + The temporary directory will be stored at /tmp/`whoami`/pytest-of-`whoami`/. There can be at most + 3 temp directories in this location so upon the 4th test run, the 1st temp directory will be removed. + + :param tmp_path_factory: A built in factory with pytest to help create temp paths for testing + :yields: The path to the temp output directory we'll use for this test run + """ + # Log the cwd, then create and move into the temporary one + cwd = os.getcwd() + temp_integration_outfile_dir = tmp_path_factory.mktemp("integration_outfiles_") + os.chdir(temp_integration_outfile_dir) + + yield temp_integration_outfile_dir + + # Move back to the directory we started at + os.chdir(cwd) + + +@pytest.fixture(scope="session") +def redis_pass() -> str: + """ + This fixture represents the password to the merlin test server. + + :returns: The redis password for our test server + """ + return "merlin-test-server" + + +@pytest.fixture(scope="session") +def merlin_server_dir(temp_output_dir: str, redis_pass: str) -> str: # pylint: disable=redefined-outer-name + """ + This fixture will initialize the merlin server (i.e. create all the files we'll + need to start up a local redis server). It will return the path to the directory + containing the files needed for the server to start up. + + :param temp_output_dir: The path to the temporary output directory we'll be using for this test run + :param redis_pass: The password to the test redis server that we'll create here + :returns: The path to the merlin_server directory with the server configurations + """ + # Initialize the setup for the local redis server + # We'll also set the password to 'merlin-test-server' so it'll be easy to shutdown if there's an issue + subprocess.run(f"merlin server init; merlin server config -pwd {redis_pass}", shell=True, capture_output=True, text=True) + + # Check that the merlin server was initialized properly + server_dir = f"{temp_output_dir}/merlin_server" + if not os.path.exists(server_dir): + raise ServerInitError("The merlin server was not initialized properly.") + + return server_dir + + +@pytest.fixture(scope="session") +def redis_server(merlin_server_dir: str, redis_pass: str) -> str: # pylint: disable=redefined-outer-name,unused-argument + """ + Start a redis server instance that runs on localhost:6379. This will yield the + redis server uri that can be used to create a connection with celery. + + :param merlin_server_dir: The directory to the merlin test server configuration. + This will not be used here but we need the server configurations before we can + start the server. + :param redis_pass: The raw redis password stored in the redis.pass file + :yields: The local redis server uri + """ + # Start the local redis server + try: + subprocess.run("merlin server start", shell=True, capture_output=True, text=True, timeout=5) + except subprocess.TimeoutExpired: + pass + + # Ensure the server started properly + host = "localhost" + port = 6379 + database = 0 + username = "default" + redis_client = redis.Redis(host=host, port=port, db=database, password=redis_pass, username=username) + if not redis_client.ping(): + raise RedisServerError("The redis server could not be pinged. Check that the server is running with 'ps ux'.") + + # Hand over the redis server url to any other fixtures/tests that need it + redis_server_uri = f"redis://{username}:{redis_pass}@{host}:{port}/{database}" + yield redis_server_uri + + # Kill the server; don't run this until all tests are done (accomplished with 'yield' above) + kill_process = subprocess.run("merlin server stop", shell=True, capture_output=True, text=True) + assert "Merlin server terminated." in kill_process.stderr + + +@pytest.fixture(scope="session") +def celery_app(redis_server: str) -> Celery: # pylint: disable=redefined-outer-name + """ + Create the celery app to be used throughout our integration tests. + + :param redis_server: The redis server uri we'll use to connect to redis + :returns: The celery app object we'll use for testing + """ + return Celery("test_app", broker=redis_server, backend=redis_server) + + +@pytest.fixture(scope="session") +def worker_queue_map() -> Dict[str, str]: + """ + Worker and queue names to be used throughout tests + + :returns: A dict of dummy worker/queue associations + """ + return {f"test_worker_{i}": f"test_queue_{i}" for i in range(3)} + + +def are_workers_ready(app: Celery, num_workers: int, verbose: bool = False) -> bool: + """ + Check to see if the workers are up and running yet. + + :param app: The celery app fixture that's connected to our redis server + :param num_workers: An int representing the number of workers we're looking to have started + :param verbose: If true, enable print statements to show where we're at in execution + :returns: True if all workers are running. False otherwise. + """ + app_stats = app.control.inspect().stats() + if verbose: + print(f"app_stats: {app_stats}") + return app_stats is not None and len(app_stats) == num_workers + + +def wait_for_worker_launch(app: Celery, num_workers: int, verbose: bool = False): + """ + Poll the workers over a fixed interval of time. If the workers don't show up + within the time limit then we'll raise a timeout error. Otherwise, the workers + are up and running and we can continue with our tests. + + :param app: The celery app fixture that's connected to our redis server + :param num_workers: An int representing the number of workers we're looking to have started + :param verbose: If true, enable print statements to show where we're at in execution + """ + max_wait_time = 2 # Maximum wait time in seconds + wait_interval = 0.5 # Interval between checks in seconds + waited_time = 0 + + if verbose: + print("waiting for workers to launch...") + + # Wait until all workers are ready + while not are_workers_ready(app, num_workers, verbose=verbose) and waited_time < max_wait_time: + sleep(wait_interval) + waited_time += wait_interval + + # If all workers are not ready after the maximum wait time, raise an error + if not are_workers_ready(app, num_workers, verbose=verbose): + raise TimeoutError("Celery workers did not start within the expected time.") + + if verbose: + print("workers launched") + + +def shutdown_processes(worker_processes: List[multiprocessing.Process], echo_processes: List[subprocess.Popen]): + """ + Given lists of processes, shut them all down. Worker processes were created with the + multiprocessing library and echo processes were created with the subprocess library, + so we have to shut them down slightly differently. + + :param worker_processes: A list of worker processes to terminate + :param echo_processes: A list of echo processes to terminate + """ + # Worker processes were created with the multiprocessing library + for worker_process in worker_processes: + # Try to terminate the process gracefully + worker_process.terminate() + process_exit_code = worker_process.join(timeout=3) + + # If it won't terminate then force kill it + if process_exit_code is None: + worker_process.kill() + + # Gracefully terminate the echo processes + for echo_process in echo_processes: + echo_process.terminate() + echo_process.wait() + + # The echo processes will spawn 3 sleep inf processes that we also need to kill + subprocess.run("ps ux | grep 'sleep inf' | grep -v grep | awk '{print $2}' | xargs kill", shell=True) + + +def start_worker(app: Celery, worker_launch_cmd: List[str]): + """ + This is where a worker is actually started. Each worker maintains control of a process until + we tell it to stop, that's why we have to use the multiprocessing library for this. We have to use + app.worker_main instead of the normal "celery -A worker" command to launch the workers + since our celery app is created in a pytest fixture and is unrecognizable by the celery command. + For each worker, the output of it's logs are sent to + /tmp/`whoami`/pytest-of-`whoami`/pytest-current/integration_outfiles_current/ under a file with a name + similar to: test_worker_*.log. + NOTE: pytest-current/ will have the results of the most recent test run. If you want to see a previous run + check under pytest-/. HOWEVER, only the 3 most recent test runs will be saved. + + :param app: The celery app fixture that's connected to our redis server + :param worker_launch_cmd: The command to launch a worker + """ + app.worker_main(worker_launch_cmd) + + +@pytest.fixture(scope="class") +def launch_workers(celery_app: Celery, worker_queue_map: Dict[str, str]): # pylint: disable=redefined-outer-name + """ + Launch the workers on the celery app fixture using the worker and queue names + defined in the worker_queue_map fixture. + + :param celery_app: The celery app fixture that's connected to our redis server + :param worker_queue_map: A dict where the keys are worker names and the values are queue names + """ + # Create the processes that will start the workers and store them in a list + worker_processes = [] + echo_processes = [] + for worker, queue in worker_queue_map.items(): + worker_launch_cmd = ["worker", "-n", worker, "-Q", queue, "--concurrency", "1", f"--logfile={worker}.log"] + + # We have to use this dummy echo command to simulate a celery worker command that will show up with 'ps ux' + # We'll sleep for infinity here and then kill this process during shutdown + echo_process = subprocess.Popen( # pylint: disable=consider-using-with + f"echo 'celery test_app {' '.join(worker_launch_cmd)}'; sleep inf", shell=True + ) + echo_processes.append(echo_process) + + # We launch workers in their own process since they maintain control of a process until we stop them + worker_process = multiprocessing.Process(target=start_worker, args=(celery_app, worker_launch_cmd)) + worker_process.start() + worker_processes.append(worker_process) + + # Ensure that the workers start properly before letting tests use them + try: + num_workers = len(worker_queue_map) + wait_for_worker_launch(celery_app, num_workers, verbose=False) + except TimeoutError as exc: + # If workers don't launch in time, we need to make sure these processes stop + shutdown_processes(worker_processes, echo_processes) + raise exc + + # Give control to the tests that need to use workers + yield + + # Shut down the workers and terminate the processes + celery_app.control.broadcast("shutdown", destination=list(worker_queue_map.keys())) + shutdown_processes(worker_processes, echo_processes) diff --git a/tests/integration/test_definitions.py b/tests/integration/definitions.py similarity index 100% rename from tests/integration/test_definitions.py rename to tests/integration/definitions.py diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index c0b699055..fcdb9e0b2 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -39,10 +39,8 @@ from contextlib import suppress from subprocess import TimeoutExpired, run -# Pylint complains that we didn't install this module but it's defined locally so ignore -from test_definitions import OUTPUT_DIR, define_tests # pylint: disable=E0401 - from merlin.display import tabulate_info +from tests.integration.definitions import OUTPUT_DIR, define_tests # pylint: disable=E0401 def get_definition_issues(test): @@ -237,7 +235,7 @@ def run_tests(args, tests): # pylint: disable=R0914 total += 1 continue dot_length = 50 - len(test_name) - len(str(test_label)) - print(f"TEST {test_label}: {test_name}{'.'*dot_length}", end="") + print(f"TEST {test_label}: {test_name}{'.' * dot_length}", end="") # Check the format of the test definition definition_issues = get_definition_issues(test) if definition_issues: diff --git a/tests/unit/study/test_celeryadapter.py b/tests/unit/study/test_celeryadapter.py new file mode 100644 index 000000000..82e8401e6 --- /dev/null +++ b/tests/unit/study/test_celeryadapter.py @@ -0,0 +1,160 @@ +############################################################################### +# Copyright (c) 2023, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory +# Written by the Merlin dev team, listed in the CONTRIBUTORS file. +# +# +# LLNL-CODE-797170 +# All rights reserved. +# This file is part of Merlin, Version: 1.11.1. +# +# For details, see https://github.com/LLNL/merlin. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### +""" +Tests for the celeryadapter module. +""" +from typing import Dict + +import celery + +from merlin.config import Config +from merlin.study import celeryadapter + + +class TestActiveQueues: + """ + This class will test queue related functions in the celeryadapter.py module. + It will run tests where we need active queues to interact with. + """ + + def test_query_celery_queues(self, launch_workers: "Fixture"): # noqa: F821 + """ + Test the query_celery_queues function by providing it with a list of active queues. + This should return a list of tuples. Each tuple will contain information + (name, num jobs, num consumers) for each queue that we provided. + """ + # TODO Modify query_celery_queues so the output for a redis broker is the same + # as the output for rabbit broker + + def test_get_running_queues(self, launch_workers: "Fixture", worker_queue_map: Dict[str, str]): # noqa: F821 + """ + Test the get_running_queues function with queues active. + This should return a list of active queues. + + :param `launch_workers`: A pytest fixture that launches celery workers for us to interact with + :param `worker_queue_map`: A pytest fixture that returns a dict of workers and queues + """ + result = celeryadapter.get_running_queues("test_app", test_mode=True) + assert sorted(result) == sorted(list(worker_queue_map.values())) + + def test_get_queues_active( + self, celery_app: celery.Celery, launch_workers: "Fixture", worker_queue_map: Dict[str, str] # noqa: F821 + ): + """ + Test the get_queues function with queues active. + This should return a tuple where the first entry is a dict of queue info + and the second entry is a list of worker names. + + :param `celery_app`: A pytest fixture for the test Celery app + :param `launch_workers`: A pytest fixture that launches celery workers for us to interact with + :param `worker_queue_map`: A pytest fixture that returns a dict of workers and queues + """ + # Start the queues and run the test + queue_result, worker_result = celeryadapter.get_queues(celery_app) + + # Ensure we got output before looping + assert len(queue_result) == len(worker_result) == 3 + + for worker, queue in worker_queue_map.items(): + # Check that the entry in the queue_result dict for this queue is correct + assert queue in queue_result + assert len(queue_result[queue]) == 1 + assert worker in queue_result[queue][0] + + # Remove this entry from the queue_result dict + del queue_result[queue] + + # Check that this worker was added to the worker_result list + worker_found = False + for worker_name in worker_result[:]: + if worker in worker_name: + worker_found = True + worker_result.remove(worker_name) + break + assert worker_found + + # Ensure there was no extra output that we weren't expecting + assert queue_result == {} + assert worker_result == [] + + +class TestInactiveQueues: + """ + This class will test queue related functions in the celeryadapter.py module. + It will run tests where we don't need any active queues to interact with. + """ + + def test_query_celery_queues(self): + """ + Test the query_celery_queues function by providing it with a list of inactive queues. + This should return a list of strings. Each string will give a message saying that a + particular queue was inactive + """ + # TODO Modify query_celery_queues so the output for a redis broker is the same + # as the output for rabbit broker + + def test_celerize_queues(self, worker_queue_map: Dict[str, str]): + """ + Test the celerize_queues function. This should add the celery queue_tag + to the front of the queues we provide it. + + :param `worker_queue_map`: A pytest fixture that returns a dict of workers and queues + """ + # Create variables to be used in the test + queue_tag = "[merlin]_" + queues_to_check = list(worker_queue_map.values()) + dummy_config = Config({"celery": {"queue_tag": queue_tag}}) + + # Run the test + celeryadapter.celerize_queues(queues_to_check, dummy_config) + + # Ensure the queue tag was added to every queue + for queue in queues_to_check: + assert queue_tag in queue + + def test_get_running_queues(self): + """ + Test the get_running_queues function with no queues active. + This should return an empty list. + """ + result = celeryadapter.get_running_queues("test_app", test_mode=True) + assert result == [] + + def test_get_queues(self, celery_app: celery.Celery): + """ + Test the get_queues function with no queues active. + This should return a tuple where the first entry is an empty dict + and the second entry is an empty list. + + :param `celery_app`: A pytest fixture for the test Celery app + """ + queue_result, worker_result = celeryadapter.get_queues(celery_app) + assert queue_result == {} + assert worker_result == [] From 38651f2650e8aba97552c4575e97d66be3205545 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:38:13 -0800 Subject: [PATCH 24/48] Bugfix for WEAVE CI (#457) * begin work on integration refactor; create fixtures and initial tests * update CHANGELOG and run fix-style * add pytest fixtures and README explaining them * add tests to demonstrate how to use the fixtures * move/rename some files and modify integration's README * add password change to redis.pass file * fix lint issues * modify redis pwd for test server to be constant for each test * fix lint issue only caught on github ci * add fix for merlin server startup * update CHANGELOG --- CHANGELOG.md | 1 + tests/conftest.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca916ea9..9db50369a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Pytest fixtures in the `conftest.py` file of the integration test suite + - NOTE: an export command `export LC_ALL='C'` had to be added to fix a bug in the WEAVE CI. This can be removed when we resolve this issue for the `merlin server` command - Tests for the `celeryadapter.py` module ## [1.11.1] diff --git a/tests/conftest.py b/tests/conftest.py index a496175eb..88932c5db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,7 +123,8 @@ def redis_server(merlin_server_dir: str, redis_pass: str) -> str: # pylint: dis """ # Start the local redis server try: - subprocess.run("merlin server start", shell=True, capture_output=True, text=True, timeout=5) + # Need to set LC_ALL='C' before starting the server or else redis causes a failure + subprocess.run("export LC_ALL='C'; merlin server start", shell=True, capture_output=True, text=True, timeout=5) except subprocess.TimeoutExpired: pass From b9afbdbe5e075236e6c1e6a86b7f4f63d5a97350 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:59:03 -0800 Subject: [PATCH 25/48] bugfix/monitor-shutdown (#452) * add celery query to see if workers still processing tasks * fix merlin status when using redis as broker * fix consumer count bug and run fix-style * fix linter issues * update changelog * update docs for monitor * remove unused exception I previously added * first attempt at using pytest fixtures for monitor tests * (partially) fix launch_workers fixture so it can be used in multiple classes * fix linter issues and typo on pytest decorator * update black's python version and fix style issue * remove print statements from celeryadapter.py * workers manager is now allowed to be used as a context manager * add one thing to changelog and remove print statement --- .github/workflows/push-pr_workflow.yml | 2 +- CHANGELOG.md | 5 + Makefile | 14 +- docs/source/merlin_commands.rst | 16 +- merlin/exceptions/__init__.py | 11 ++ merlin/main.py | 12 +- merlin/router.py | 163 +++++++++++++---- merlin/study/celeryadapter.py | 99 ++++++++--- requirements/dev.txt | 1 + tests/celery_test_workers.py | 231 +++++++++++++++++++++++++ tests/conftest.py | 157 ++++------------- tests/unit/study/test_celeryadapter.py | 148 +++++++++++++--- 12 files changed, 632 insertions(+), 227 deletions(-) create mode 100644 tests/celery_test_workers.py diff --git a/.github/workflows/push-pr_workflow.yml b/.github/workflows/push-pr_workflow.yml index e2d9e164e..bef6f8608 100644 --- a/.github/workflows/push-pr_workflow.yml +++ b/.github/workflows/push-pr_workflow.yml @@ -129,7 +129,7 @@ jobs: - name: Run pytest over unit test suite run: | - python3 -m pytest tests/unit/ + python3 -m pytest -v --order-scope=module tests/unit/ - name: Run integration test suite for local tests run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db50369a..bf0074e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pytest fixtures in the `conftest.py` file of the integration test suite - NOTE: an export command `export LC_ALL='C'` had to be added to fix a bug in the WEAVE CI. This can be removed when we resolve this issue for the `merlin server` command - Tests for the `celeryadapter.py` module +- New CeleryTestWorkersManager context to help with starting/stopping workers for tests + +### Fixed +- The `merlin status` command so that it's consistent in its output whether using redis or rabbitmq as the broker +- The `merlin monitor` command will now keep an allocation up if the queues are empty and workers are still processing tasks ## [1.11.1] ### Fixed diff --git a/Makefile b/Makefile index 2f9db031b..b669d51b1 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ install-dev: virtualenv install-merlin install-workflow-deps # tests require a valid dev install of merlin unit-tests: . $(VENV)/bin/activate; \ - $(PYTHON) -m pytest $(UNIT); \ + $(PYTHON) -m pytest -v --order-scope=module $(UNIT); \ # run CLI tests - these require an active install of merlin in a venv @@ -135,9 +135,9 @@ check-flake8: check-black: . $(VENV)/bin/activate; \ - $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py36 $(MRLN); \ - $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py36 $(TEST); \ - $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py36 *.py; \ + $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py38 $(MRLN); \ + $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py38 $(TEST); \ + $(PYTHON) -m black --check --line-length $(MAX_LINE_LENGTH) --target-version py38 *.py; \ check-isort: @@ -179,9 +179,9 @@ fix-style: $(PYTHON) -m isort -w $(MAX_LINE_LENGTH) $(MRLN); \ $(PYTHON) -m isort -w $(MAX_LINE_LENGTH) $(TEST); \ $(PYTHON) -m isort -w $(MAX_LINE_LENGTH) *.py; \ - $(PYTHON) -m black --target-version py36 -l $(MAX_LINE_LENGTH) $(MRLN); \ - $(PYTHON) -m black --target-version py36 -l $(MAX_LINE_LENGTH) $(TEST); \ - $(PYTHON) -m black --target-version py36 -l $(MAX_LINE_LENGTH) *.py; \ + $(PYTHON) -m black --target-version py38 -l $(MAX_LINE_LENGTH) $(MRLN); \ + $(PYTHON) -m black --target-version py38 -l $(MAX_LINE_LENGTH) $(TEST); \ + $(PYTHON) -m black --target-version py38 -l $(MAX_LINE_LENGTH) *.py; \ # Increment the Merlin version. USE ONLY ON DEVELOP BEFORE MERGING TO MASTER. diff --git a/docs/source/merlin_commands.rst b/docs/source/merlin_commands.rst index cb9b8eefb..1baa0e7a5 100644 --- a/docs/source/merlin_commands.rst +++ b/docs/source/merlin_commands.rst @@ -110,8 +110,15 @@ Monitor (``merlin monitor``) Batch submission scripts may not keep the batch allocation alive if there is not a blocking process in the submission script. The ``merlin monitor`` command addresses this by providing a blocking process that -checks for tasks in the queues every (sleep) seconds. When the queues are empty, the -blocking process will exit and allow the allocation to end. +checks for tasks in the queues every (sleep) seconds. When the queues are empty, the +monitor will query celery to see if any workers are still processing tasks from the +queues. If no workers are processing any tasks from the queues and the queues are empty, +the blocking process will exit and allow the allocation to end. + +The ``monitor`` function will check for celery workers for up to +10*(sleep) seconds before monitoring begins. The loop happens when the +queue(s) in the spec contain tasks, but no running workers are detected. +This is to protect against a failed worker launch. .. code:: bash @@ -129,11 +136,6 @@ for workers. The default is 60 seconds. The only currently available option for ``--task_server`` is celery, which is the default when this flag is excluded. -The ``monitor`` function will check for celery workers for up to -10*(sleep) seconds before monitoring begins. The loop happens when the -queue(s) in the spec contain tasks, but no running workers are detected. -This is to protect against a failed worker launch. - Purging Tasks (``merlin purge``) -------------------------------- diff --git a/merlin/exceptions/__init__.py b/merlin/exceptions/__init__.py index 72d5a1521..9bc7803a9 100644 --- a/merlin/exceptions/__init__.py +++ b/merlin/exceptions/__init__.py @@ -42,6 +42,7 @@ "HardFailException", "InvalidChainException", "RestartException", + "NoWorkersException", ) @@ -92,3 +93,13 @@ class RestartException(Exception): def __init__(self): super().__init__() + + +class NoWorkersException(Exception): + """ + Exception to signal that no workers were started + to process a non-empty queue(s). + """ + + def __init__(self, message): + super().__init__(message) diff --git a/merlin/main.py b/merlin/main.py index 55496a72c..0ee2e36ce 100644 --- a/merlin/main.py +++ b/merlin/main.py @@ -256,8 +256,9 @@ def query_status(args): print(banner_small) spec, _ = get_merlin_spec_with_override(args) ret = router.query_status(args.task_server, spec, args.steps) - for name, jobs, consumers in ret: - print(f"{name:30} - Workers: {consumers:10} - Queued Tasks: {jobs:10}") + + for name, queue_info in ret.items(): + print(f"{name:30} - Workers: {queue_info['consumers']:10} - Queued Tasks: {queue_info['jobs']:10}") if args.csv is not None: router.dump_status(ret, args.csv) @@ -353,8 +354,13 @@ def process_monitor(args): """ LOG.info("Monitor: checking queues ...") spec, _ = get_merlin_spec_with_override(args) + + # Give the user time to queue up jobs in case they haven't already + time.sleep(args.sleep) + + # Check if we still need our allocation while router.check_merlin_status(args, spec): - LOG.info("Monitor: found tasks in queues") + LOG.info("Monitor: found tasks in queues and/or tasks being processed") time.sleep(args.sleep) LOG.info("Monitor: ... stop condition met") diff --git a/merlin/router.py b/merlin/router.py index 01a10aae7..6c90c1d80 100644 --- a/merlin/router.py +++ b/merlin/router.py @@ -39,9 +39,13 @@ import os import time from datetime import datetime +from typing import Dict, List +from merlin.exceptions import NoWorkersException from merlin.study.celeryadapter import ( + check_celery_workers_processing, create_celery_config, + get_active_celery_queues, get_workers_from_app, purge_celery_tasks, query_celery_queues, @@ -151,12 +155,12 @@ def dump_status(query_return, csv_file): with open(csv_file, mode=fmode) as f: # pylint: disable=W1514,C0103 if f.mode == "w": # add the header f.write("# time") - for name, job, consumer in query_return: + for name in query_return: f.write(f",{name}:tasks,{name}:consumers") f.write("\n") f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - for name, job, consumer in query_return: - f.write(f",{job},{consumer}") + for queue_info in query_return.values(): + f.write(f",{queue_info['jobs']},{queue_info['consumers']}") f.write("\n") @@ -236,43 +240,130 @@ def create_config(task_server: str, config_dir: str, broker: str, test: str) -> LOG.error("Only celery can be configured currently.") -def check_merlin_status(args, spec): +def get_active_queues(task_server: str) -> Dict[str, List[str]]: """ - Function to check merlin workers and queues to keep - the allocation alive + Get a dictionary of active queues and the workers attached to these queues. + + :param `task_server`: The task server to query for active queues + :returns: A dict where keys are queue names and values are a list of workers watching them + """ + active_queues = {} + + if task_server == "celery": + from merlin.celery import app # pylint: disable=C0415 + + active_queues, _ = get_active_celery_queues(app) + else: + LOG.error("Only celery can be configured currently.") + + return active_queues + + +def wait_for_workers(sleep: int, task_server: str, spec: "MerlinSpec"): # noqa + """ + Wait on workers to start up. Check on worker start 10 times with `sleep` seconds between + each check. If no workers are started in time, raise an error to kill the monitor (there + was likely an issue with the task server that caused worker launch to fail). + + :param `sleep`: An integer representing the amount of seconds to sleep between each check + :param `task_server`: The task server from which to look for workers + :param `spec`: A MerlinSpec object representing the spec we're monitoring + """ + # Get the names of the workers that we're looking for + worker_names = spec.get_worker_names() + LOG.info(f"Checking for the following workers: {worker_names}") + + # Loop until workers are detected + count = 0 + max_count = 10 + while count < max_count: + # This list will include strings comprised of the worker name with the hostname e.g. worker_name@host. + worker_status = get_workers(task_server) + LOG.info(f"Monitor: checking for workers, running workers = {worker_status} ...") + + # Check to see if any of the workers we're looking for in 'worker_names' have started + check = any(any(iwn in iws for iws in worker_status) for iwn in worker_names) + if check: + break + + # Increment count and sleep until the next check + count += 1 + time.sleep(sleep) + + # If no workers were started in time, raise an exception to stop the monitor + if count == max_count: + raise NoWorkersException("Monitor: no workers available to process the non-empty queue") + + +def check_workers_processing(queues_in_spec: List[str], task_server: str) -> bool: + """ + Check if any workers are still processing tasks by querying the task server. + + :param `queues_in_spec`: A list of queues to check if tasks are still active in + :param `task_server`: The task server from which to query + :returns: True if workers are still processing tasks, False otherwise + """ + result = False + + if task_server == "celery": + from merlin.celery import app + + result = check_celery_workers_processing(queues_in_spec, app) + else: + LOG.error("Celery is not specified as the task server!") + + return result + + +def check_merlin_status(args: "Namespace", spec: "MerlinSpec") -> bool: # noqa + """ + Function to check merlin workers and queues to keep the allocation alive :param `args`: parsed CLI arguments - :param `spec`: the parsed spec.yaml + :param `spec`: the parsed spec.yaml as a MerlinSpec object + :returns: True if there are still tasks being processed, False otherwise """ + # Initialize the variable to track if there are still active tasks + active_tasks = False + + # Get info about jobs and workers in our spec from celery queue_status = query_status(args.task_server, spec, args.steps, verbose=False) + LOG.debug(f"Monitor: queue_status: {queue_status}") + # Count the number of jobs that are active + # (Adding up the number of consumers in the same way is inaccurate so we won't do that) total_jobs = 0 - total_consumers = 0 - for _, jobs, consumers in queue_status: - total_jobs += jobs - total_consumers += consumers - - if total_jobs > 0 and total_consumers == 0: - # Determine if any of the workers are on this allocation - worker_names = spec.get_worker_names() - - # Loop until workers are detected. - count = 0 - max_count = 10 - while count < max_count: - # This list will include strings comprised of the worker name with the hostname e.g. worker_name@host. - worker_status = get_workers(args.task_server) - LOG.info(f"Monitor: checking for workers, running workers = {worker_status} ...") - - check = any(any(iwn in iws for iws in worker_status) for iwn in worker_names) - if check: - break - - count += 1 - time.sleep(args.sleep) - - if count == max_count: - LOG.error("Monitor: no workers available to process the non-empty queue") - total_jobs = 0 - - return total_jobs + for queue_info in queue_status.values(): + total_jobs += queue_info["jobs"] + + # Get the queues defined in the spec + queues_in_spec = spec.get_queue_list(["all"]) + LOG.debug(f"Monitor: queues_in_spec: {queues_in_spec}") + + # Get the active queues and the workers that are watching them + active_queues = get_active_queues(args.task_server) + LOG.debug(f"Monitor: active_queues: {active_queues}") + + # Count the number of workers that are active + consumers = set() + for active_queue, workers_on_queue in active_queues.items(): + if active_queue in queues_in_spec: + consumers |= set(workers_on_queue) + LOG.debug(f"Monitor: consumers found: {consumers}") + total_consumers = len(consumers) + + LOG.info(f"Monitor: found {total_jobs} jobs in queues and {total_consumers} workers alive") + + # If there are no workers, wait for the workers to start + if total_consumers == 0: + wait_for_workers(args.sleep, args.task_server, spec) + + # If we're here, workers have started and jobs should be queued + if total_jobs > 0: + active_tasks = True + # If there are no jobs left, see if any workers are still processing them + elif total_jobs == 0: + active_tasks = check_workers_processing(queues_in_spec, args.task_server) + + LOG.debug(f"Monitor: active_tasks: {active_tasks}") + return active_tasks diff --git a/merlin/study/celeryadapter.py b/merlin/study/celeryadapter.py index cd9714dff..84fb96ff1 100644 --- a/merlin/study/celeryadapter.py +++ b/merlin/study/celeryadapter.py @@ -39,12 +39,18 @@ from contextlib import suppress from typing import Dict, List, Optional +from amqp.exceptions import ChannelError +from celery import Celery + +from merlin.config import Config from merlin.study.batch import batch_check_parallel, batch_worker_launch from merlin.utils import apply_list_of_regex, check_machines, get_procs, get_yaml_var, is_running LOG = logging.getLogger(__name__) +# TODO figure out a better way to handle the import of celery app and CONFIG + def run_celery(study, run_mode=None): """ @@ -102,7 +108,7 @@ def get_running_queues(celery_app_name: str, test_mode: bool = False) -> List[st return running_queues -def get_queues(app): +def get_active_celery_queues(app): """Get all active queues and workers for a celery application. Unlike get_running_queues, this goes through the application's server. @@ -117,7 +123,7 @@ def get_queues(app): :example: >>> from merlin.celery import app - >>> queues, workers = get_queues(app) + >>> queues, workers = get_active_celery_queues(app) >>> queue_names = [*queues] >>> workers_on_q0 = queues[queue_names[0]] >>> workers_not_on_q0 = [worker for worker in workers @@ -139,7 +145,7 @@ def get_queues(app): def get_active_workers(app): """ - This is the inverse of get_queues() defined above. This function + This is the inverse of get_active_celery_queues() defined above. This function builds a dict where the keys are worker names and the values are lists of queues attached to the worker. @@ -228,7 +234,7 @@ def query_celery_workers(spec_worker_names, queues, workers_regex): # --queues flag if queues: # Get a mapping between queues and the workers watching them - queue_worker_map, _ = get_queues(app) + queue_worker_map, _ = get_active_celery_queues(app) # Remove duplicates and prepend the celery queue tag to all queues queues = list(set(queues)) celerize_queues(queues) @@ -270,26 +276,54 @@ def query_celery_workers(spec_worker_names, queues, workers_regex): print() -def query_celery_queues(queues): - """Return stats for queues specified. +def query_celery_queues(queues: List[str], app: Celery = None, config: Config = None) -> Dict[str, List[str]]: + """ + Build a dict of information about the number of jobs and consumers attached + to specific queues that we want information on. - Send results to the log. + :param queues: A list of the queues we want to know about + :param app: The celery application (this will be none unless testing) + :param config: The configuration object that has the broker name (this will be none unless testing) + :returns: A dict of info on the number of jobs and consumers for each queue in `queues` """ - from merlin.celery import app # pylint: disable=C0415 + if app is None: + from merlin.celery import app # pylint: disable=C0415 + if config is None: + from merlin.config.configfile import CONFIG as config # pylint: disable=C0415 - connection = app.connection() - found_queues = [] - try: - channel = connection.channel() - for queue in queues: - try: - name, jobs, consumers = channel.queue_declare(queue=queue, passive=True) - found_queues.append((name, jobs, consumers)) - except Exception as e: # pylint: disable=C0103,W0718 - LOG.warning(f"Cannot find queue {queue} on server.{e}") - finally: - connection.close() - return found_queues + # Initialize the dictionary with the info we want about our queues + queue_info = {queue: {"consumers": 0, "jobs": 0} for queue in queues} + + # Open a connection via our Celery app + with app.connection() as conn: + # Open a channel inside our connection + with conn.channel() as channel: + # Loop through all the queues we're searching for + for queue in queues: + try: + # Count the number of jobs and consumers for each queue + _, queue_info[queue]["jobs"], queue_info[queue]["consumers"] = channel.queue_declare( + queue=queue, passive=True + ) + # Redis likes to throw this error when a queue we're looking for has no jobs + except ChannelError: + pass + + # Redis doesn't keep track of consumers attached to queues like rabbit does + # so we have to count this ourselves here + if config.broker.name in ("rediss", "redis"): + # Get a dict of active queues by querying the celery app + active_queues = app.control.inspect().active_queues() + if active_queues is not None: + # Loop through each active queue that was found + for active_queue_list in active_queues.values(): + # Loop through each queue that each worker is watching + for active_queue in active_queue_list: + # If this is a queue we're looking for, increment the consumer count + if active_queue["name"] in queues: + queue_info[active_queue["name"]]["consumers"] += 1 + + return queue_info def get_workers_from_app(): @@ -308,6 +342,27 @@ def get_workers_from_app(): return [*workers] +def check_celery_workers_processing(queues_in_spec: List[str], app: Celery) -> bool: + """ + Query celery to see if any workers are still processing tasks. + + :param queues_in_spec: A list of queues to check if tasks are still active in + :param app: The celery app that we're querying + :returns: True if workers are still processing tasks, False otherwise + """ + # Query celery for active tasks + active_tasks = app.control.inspect().active() + + # Search for the queues we provided if necessary + if active_tasks is not None: + for tasks in active_tasks.values(): + for task in tasks: + if task["delivery_info"]["routing_key"] in queues_in_spec: + return True + + return False + + def _get_workers_to_start(spec, steps): """ Helper function to return a set of workers to start based on @@ -620,7 +675,7 @@ def stop_celery_workers(queues=None, spec_worker_names=None, worker_regex=None): from merlin.celery import app # pylint: disable=C0415 LOG.debug(f"Sending stop to queues: {queues}, worker_regex: {worker_regex}, spec_worker_names: {spec_worker_names}") - active_queues, _ = get_queues(app) + active_queues, _ = get_active_celery_queues(app) # If not specified, get all the queues if queues is None: diff --git a/requirements/dev.txt b/requirements/dev.txt index 895a89249..6e8722b4b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -11,3 +11,4 @@ sphinx>=2.0.0 alabaster johnnydep deepdiff +pytest-order diff --git a/tests/celery_test_workers.py b/tests/celery_test_workers.py new file mode 100644 index 000000000..39eb2a39b --- /dev/null +++ b/tests/celery_test_workers.py @@ -0,0 +1,231 @@ +############################################################################### +# Copyright (c) 2023, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory +# Written by the Merlin dev team, listed in the CONTRIBUTORS file. +# +# +# LLNL-CODE-797170 +# All rights reserved. +# This file is part of Merlin, Version: 1.11.1. +# +# For details, see https://github.com/LLNL/merlin. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### +""" +Module to define functionality for test workers and how to start/stop +them in their own processes. +""" +import multiprocessing +import os +import signal +import subprocess +from time import sleep +from types import TracebackType +from typing import Dict, List, Type + +from celery import Celery + + +class CeleryTestWorkersManager: + """ + A class to handle the setup and teardown of celery workers. + This should be treated as a context and used with python's + built-in 'with' statement. If you use it without this statement, + beware that the processes spun up here may never be stopped. + """ + + def __init__(self, app: Celery): + self.app = app + self.running_workers = [] + self.worker_processes = {} + self.echo_processes = {} + + def __enter__(self): + """This magic method is necessary for allowing this class to be used as a context manager.""" + return self + + def __exit__(self, exc_type: Type[Exception], exc_value: Exception, traceback: TracebackType): + """ + This will always run at the end of a context with statement, even if an error is raised. + It's a safe way to ensure all of our subprocesses are stopped no matter what. + """ + + # Try to stop everything gracefully first + self.stop_all_workers() + + # Check that all the worker processes were stopped, otherwise forcefully terminate them + for worker_process in self.worker_processes.values(): + if worker_process.is_alive(): + worker_process.kill() + + # Check that all the echo processes were stopped, otherwise forcefully terminate them + ps_proc = subprocess.run("ps ux", shell=True, capture_output=True, text=True) + for pid in self.echo_processes.values(): + if str(pid) in ps_proc.stdout: + os.kill(pid, signal.SIGKILL) + + def _is_worker_ready(self, worker_name: str, verbose: bool = False) -> bool: + """ + Check to see if the worker is up and running yet. + + :param worker_name: The name of the worker we're checking on + :param verbose: If true, enable print statements to show where we're at in execution + :returns: True if the worker is running. False otherwise. + """ + ping = self.app.control.inspect().ping(destination=[f"celery@{worker_name}"]) + if verbose: + print(f"ping: {ping}") + return ping is not None and f"celery@{worker_name}" in ping + + def _wait_for_worker_launch(self, worker_name: str, verbose: bool = False): + """ + Poll the worker over a fixed interval of time. If the worker doesn't show up + within the time limit then we'll raise a timeout error. Otherwise, the worker + is up and running and we can continue with our tests. + + :param worker_name: The name of the worker we're checking on + :param verbose: If true, enable print statements to show where we're at in execution + """ + max_wait_time = 2 # Maximum wait time in seconds + wait_interval = 0.5 # Interval between checks in seconds + waited_time = 0 + worker_ready = False + + if verbose: + print(f"waiting for {worker_name} to launch...") + + # Wait until the worker is ready + while waited_time < max_wait_time: + if self._is_worker_ready(worker_name, verbose=verbose): + worker_ready = True + break + + sleep(wait_interval) + waited_time += wait_interval + + if not worker_ready: + raise TimeoutError("Celery workers did not start within the expected time.") + + if verbose: + print(f"{worker_name} launched") + + def start_worker(self, worker_launch_cmd: List[str]): + """ + This is where a worker is actually started. Each worker maintains control of a process until + we tell it to stop, that's why we have to use the multiprocessing library for this. We have to use + app.worker_main instead of the normal "celery -A worker" command to launch the workers + since our celery app is created in a pytest fixture and is unrecognizable by the celery command. + For each worker, the output of it's logs are sent to + /tmp/`whoami`/pytest-of-`whoami`/pytest-current/integration_outfiles_current/ under a file with a name + similar to: test_worker_*.log. + NOTE: pytest-current/ will have the results of the most recent test run. If you want to see a previous run + check under pytest-/. HOWEVER, only the 3 most recent test runs will be saved. + + :param worker_launch_cmd: The command to launch a worker + """ + self.app.worker_main(worker_launch_cmd) + + def launch_worker(self, worker_name: str, queues: List[str], concurrency: int = 1): + """ + Launch a single worker. We'll add the process that the worker is running in to the list of worker processes. + We'll also create an echo process to simulate a celery worker command that will show up with 'ps ux'. + + :param worker_name: The name to give to the worker + :param queues: A list of queues that the worker will be watching + :param concurrency: The concurrency value of the worker (how many child processes to have the worker spin up) + """ + # Check to make sure we have a unique worker name so we can track all processes + if worker_name in self.worker_processes: + self.stop_all_workers() + raise ValueError(f"The worker {worker_name} is already running. Choose a different name.") + + # Create the launch command for this worker + worker_launch_cmd = [ + "worker", + "-n", + worker_name, + "-Q", + ",".join(queues), + "--concurrency", + str(concurrency), + f"--logfile={worker_name}.log", + "--loglevel=DEBUG", + ] + + # Create an echo command to simulate a running celery worker since our celery worker will be spun up in + # a different process and we won't be able to see it with 'ps ux' like we normally would + echo_process = subprocess.Popen( # pylint: disable=consider-using-with + f"echo 'celery merlin_test_app {' '.join(worker_launch_cmd)}'; sleep inf", + shell=True, + preexec_fn=os.setpgrp, # Make this the parent of the group so we can kill the 'sleep inf' that's spun up + ) + self.echo_processes[worker_name] = echo_process.pid + + # Start the worker in a separate process since it'll take control of the entire process until we kill it + worker_process = multiprocessing.Process(target=self.start_worker, args=(worker_launch_cmd,)) + worker_process.start() + self.worker_processes[worker_name] = worker_process + self.running_workers.append(worker_name) + + # Wait for the worker to launch properly + try: + self._wait_for_worker_launch(worker_name, verbose=False) + except TimeoutError as exc: + self.stop_all_workers() + raise exc + + def launch_workers(self, worker_info: Dict[str, Dict]): + """ + Launch multiple workers. This will call `launch_worker` to launch each worker + individually. + + :param worker_info: A dict of worker info with the form + {"worker_name": {"concurrency": , "queues": }} + """ + for worker_name, worker_settings in worker_info.items(): + self.launch_worker(worker_name, worker_settings["queues"], worker_settings["concurrency"]) + + def stop_worker(self, worker_name: str): + """ + Stop a single running worker and its associated processes. + + :param worker_name: The name of the worker to shutdown + """ + # Send a shutdown signal to the worker + self.app.control.broadcast("shutdown", destination=[f"celery@{worker_name}"]) + + # Try to terminate the process gracefully + if self.worker_processes[worker_name] is not None: + self.worker_processes[worker_name].terminate() + process_exit_code = self.worker_processes[worker_name].join(timeout=3) + + # If it won't terminate then force kill it + if process_exit_code is None: + self.worker_processes[worker_name].kill() + + # Terminate the echo process and its sleep inf subprocess + os.killpg(os.getpgid(self.echo_processes[worker_name]), signal.SIGTERM) + sleep(2) + + def stop_all_workers(self): + """ + Stop all of the running workers and the processes associated with them. + """ + for worker_name in self.running_workers: + self.stop_worker(worker_name) diff --git a/tests/conftest.py b/tests/conftest.py index 88932c5db..38c6b0334 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,16 +31,18 @@ This module contains pytest fixtures to be used throughout the entire integration test suite. """ -import multiprocessing import os import subprocess from time import sleep -from typing import Dict, List +from typing import Dict import pytest import redis from _pytest.tmpdir import TempPathFactory from celery import Celery +from celery.canvas import Signature + +from tests.celery_test_workers import CeleryTestWorkersManager class RedisServerError(Exception): @@ -124,7 +126,7 @@ def redis_server(merlin_server_dir: str, redis_pass: str) -> str: # pylint: dis # Start the local redis server try: # Need to set LC_ALL='C' before starting the server or else redis causes a failure - subprocess.run("export LC_ALL='C'; merlin server start", shell=True, capture_output=True, text=True, timeout=5) + subprocess.run("export LC_ALL='C'; merlin server start", shell=True, timeout=5) except subprocess.TimeoutExpired: pass @@ -154,108 +156,39 @@ def celery_app(redis_server: str) -> Celery: # pylint: disable=redefined-outer- :param redis_server: The redis server uri we'll use to connect to redis :returns: The celery app object we'll use for testing """ - return Celery("test_app", broker=redis_server, backend=redis_server) + return Celery("merlin_test_app", broker=redis_server, backend=redis_server) @pytest.fixture(scope="session") -def worker_queue_map() -> Dict[str, str]: - """ - Worker and queue names to be used throughout tests - - :returns: A dict of dummy worker/queue associations - """ - return {f"test_worker_{i}": f"test_queue_{i}" for i in range(3)} - - -def are_workers_ready(app: Celery, num_workers: int, verbose: bool = False) -> bool: - """ - Check to see if the workers are up and running yet. - - :param app: The celery app fixture that's connected to our redis server - :param num_workers: An int representing the number of workers we're looking to have started - :param verbose: If true, enable print statements to show where we're at in execution - :returns: True if all workers are running. False otherwise. +def sleep_sig(celery_app: Celery) -> Signature: # pylint: disable=redefined-outer-name """ - app_stats = app.control.inspect().stats() - if verbose: - print(f"app_stats: {app_stats}") - return app_stats is not None and len(app_stats) == num_workers + Create a task registered to our celery app and return a signature for it. + Once requested by a test, you can set the queue you'd like to send this to + with `sleep_sig.set(queue=)`. Here, will likely be + one of the queues defined in the `worker_queue_map` fixture. - -def wait_for_worker_launch(app: Celery, num_workers: int, verbose: bool = False): - """ - Poll the workers over a fixed interval of time. If the workers don't show up - within the time limit then we'll raise a timeout error. Otherwise, the workers - are up and running and we can continue with our tests. - - :param app: The celery app fixture that's connected to our redis server - :param num_workers: An int representing the number of workers we're looking to have started - :param verbose: If true, enable print statements to show where we're at in execution + :param celery_app: The celery app object we'll use for testing + :returns: A celery signature for a task that will sleep for 3 seconds """ - max_wait_time = 2 # Maximum wait time in seconds - wait_interval = 0.5 # Interval between checks in seconds - waited_time = 0 - - if verbose: - print("waiting for workers to launch...") - # Wait until all workers are ready - while not are_workers_ready(app, num_workers, verbose=verbose) and waited_time < max_wait_time: - sleep(wait_interval) - waited_time += wait_interval + # Create a celery task that sleeps for 3 sec + @celery_app.task + def sleep_task(): + print("running sleep task") + sleep(3) - # If all workers are not ready after the maximum wait time, raise an error - if not are_workers_ready(app, num_workers, verbose=verbose): - raise TimeoutError("Celery workers did not start within the expected time.") + # Create a signature for this task + return sleep_task.s() - if verbose: - print("workers launched") - -def shutdown_processes(worker_processes: List[multiprocessing.Process], echo_processes: List[subprocess.Popen]): - """ - Given lists of processes, shut them all down. Worker processes were created with the - multiprocessing library and echo processes were created with the subprocess library, - so we have to shut them down slightly differently. - - :param worker_processes: A list of worker processes to terminate - :param echo_processes: A list of echo processes to terminate +@pytest.fixture(scope="session") +def worker_queue_map() -> Dict[str, str]: """ - # Worker processes were created with the multiprocessing library - for worker_process in worker_processes: - # Try to terminate the process gracefully - worker_process.terminate() - process_exit_code = worker_process.join(timeout=3) - - # If it won't terminate then force kill it - if process_exit_code is None: - worker_process.kill() - - # Gracefully terminate the echo processes - for echo_process in echo_processes: - echo_process.terminate() - echo_process.wait() - - # The echo processes will spawn 3 sleep inf processes that we also need to kill - subprocess.run("ps ux | grep 'sleep inf' | grep -v grep | awk '{print $2}' | xargs kill", shell=True) - + Worker and queue names to be used throughout tests -def start_worker(app: Celery, worker_launch_cmd: List[str]): - """ - This is where a worker is actually started. Each worker maintains control of a process until - we tell it to stop, that's why we have to use the multiprocessing library for this. We have to use - app.worker_main instead of the normal "celery -A worker" command to launch the workers - since our celery app is created in a pytest fixture and is unrecognizable by the celery command. - For each worker, the output of it's logs are sent to - /tmp/`whoami`/pytest-of-`whoami`/pytest-current/integration_outfiles_current/ under a file with a name - similar to: test_worker_*.log. - NOTE: pytest-current/ will have the results of the most recent test run. If you want to see a previous run - check under pytest-/. HOWEVER, only the 3 most recent test runs will be saved. - - :param app: The celery app fixture that's connected to our redis server - :param worker_launch_cmd: The command to launch a worker + :returns: A dict of dummy worker/queue associations """ - app.worker_main(worker_launch_cmd) + return {f"test_worker_{i}": f"test_queue_{i}" for i in range(3)} @pytest.fixture(scope="class") @@ -267,36 +200,10 @@ def launch_workers(celery_app: Celery, worker_queue_map: Dict[str, str]): # pyl :param celery_app: The celery app fixture that's connected to our redis server :param worker_queue_map: A dict where the keys are worker names and the values are queue names """ - # Create the processes that will start the workers and store them in a list - worker_processes = [] - echo_processes = [] - for worker, queue in worker_queue_map.items(): - worker_launch_cmd = ["worker", "-n", worker, "-Q", queue, "--concurrency", "1", f"--logfile={worker}.log"] - - # We have to use this dummy echo command to simulate a celery worker command that will show up with 'ps ux' - # We'll sleep for infinity here and then kill this process during shutdown - echo_process = subprocess.Popen( # pylint: disable=consider-using-with - f"echo 'celery test_app {' '.join(worker_launch_cmd)}'; sleep inf", shell=True - ) - echo_processes.append(echo_process) - - # We launch workers in their own process since they maintain control of a process until we stop them - worker_process = multiprocessing.Process(target=start_worker, args=(celery_app, worker_launch_cmd)) - worker_process.start() - worker_processes.append(worker_process) - - # Ensure that the workers start properly before letting tests use them - try: - num_workers = len(worker_queue_map) - wait_for_worker_launch(celery_app, num_workers, verbose=False) - except TimeoutError as exc: - # If workers don't launch in time, we need to make sure these processes stop - shutdown_processes(worker_processes, echo_processes) - raise exc - - # Give control to the tests that need to use workers - yield - - # Shut down the workers and terminate the processes - celery_app.control.broadcast("shutdown", destination=list(worker_queue_map.keys())) - shutdown_processes(worker_processes, echo_processes) + # Format worker info in a format the our workers manager will be able to read + # (basically just add in concurrency value to worker_queue_map) + worker_info = {worker_name: {"concurrency": 1, "queues": [queue]} for worker_name, queue in worker_queue_map.items()} + + with CeleryTestWorkersManager(celery_app) as workers_manager: + workers_manager.launch_workers(worker_info) + yield diff --git a/tests/unit/study/test_celeryadapter.py b/tests/unit/study/test_celeryadapter.py index 82e8401e6..67728881e 100644 --- a/tests/unit/study/test_celeryadapter.py +++ b/tests/unit/study/test_celeryadapter.py @@ -30,28 +30,55 @@ """ Tests for the celeryadapter module. """ +from time import sleep from typing import Dict -import celery +import pytest +from celery import Celery +from celery.canvas import Signature from merlin.config import Config from merlin.study import celeryadapter -class TestActiveQueues: +@pytest.mark.order(before="TestInactive") +class TestActive: """ - This class will test queue related functions in the celeryadapter.py module. - It will run tests where we need active queues to interact with. + This class will test functions in the celeryadapter.py module. + It will run tests where we need active queues/workers to interact with. + + NOTE: The tests in this class must be ran before the TestInactive class or else the + Celery workers needed for this class don't start + + TODO: fix the bug noted above and then check if we still need pytest-order """ - def test_query_celery_queues(self, launch_workers: "Fixture"): # noqa: F821 + def test_query_celery_queues( + self, celery_app: Celery, launch_workers: "Fixture", worker_queue_map: Dict[str, str] # noqa: F821 + ): """ Test the query_celery_queues function by providing it with a list of active queues. - This should return a list of tuples. Each tuple will contain information - (name, num jobs, num consumers) for each queue that we provided. + This should return a dict where keys are queue names and values are more dicts containing + the number of jobs and consumers in that queue. + + :param `celery_app`: A pytest fixture for the test Celery app + :param launch_workers: A pytest fixture that launches celery workers for us to interact with + :param worker_queue_map: A pytest fixture that returns a dict of workers and queues """ - # TODO Modify query_celery_queues so the output for a redis broker is the same - # as the output for rabbit broker + # Set up a dummy configuration to use in the test + dummy_config = Config({"broker": {"name": "redis"}}) + + # Get the actual output + queues_to_query = list(worker_queue_map.values()) + actual_queue_info = celeryadapter.query_celery_queues(queues_to_query, app=celery_app, config=dummy_config) + + # Ensure all 3 queues in worker_queue_map were queried before looping + assert len(actual_queue_info) == 3 + + # Ensure each queue has a worker attached + for queue_name, queue_info in actual_queue_info.items(): + assert queue_name in worker_queue_map.values() + assert queue_info == {"consumers": 1, "jobs": 0} def test_get_running_queues(self, launch_workers: "Fixture", worker_queue_map: Dict[str, str]): # noqa: F821 """ @@ -61,14 +88,14 @@ def test_get_running_queues(self, launch_workers: "Fixture", worker_queue_map: D :param `launch_workers`: A pytest fixture that launches celery workers for us to interact with :param `worker_queue_map`: A pytest fixture that returns a dict of workers and queues """ - result = celeryadapter.get_running_queues("test_app", test_mode=True) + result = celeryadapter.get_running_queues("merlin_test_app", test_mode=True) assert sorted(result) == sorted(list(worker_queue_map.values())) - def test_get_queues_active( - self, celery_app: celery.Celery, launch_workers: "Fixture", worker_queue_map: Dict[str, str] # noqa: F821 + def test_get_active_celery_queues( + self, celery_app: Celery, launch_workers: "Fixture", worker_queue_map: Dict[str, str] # noqa: F821 ): """ - Test the get_queues function with queues active. + Test the get_active_celery_queues function with queues active. This should return a tuple where the first entry is a dict of queue info and the second entry is a list of worker names. @@ -77,7 +104,7 @@ def test_get_queues_active( :param `worker_queue_map`: A pytest fixture that returns a dict of workers and queues """ # Start the queues and run the test - queue_result, worker_result = celeryadapter.get_queues(celery_app) + queue_result, worker_result = celeryadapter.get_active_celery_queues(celery_app) # Ensure we got output before looping assert len(queue_result) == len(worker_result) == 3 @@ -104,21 +131,78 @@ def test_get_queues_active( assert queue_result == {} assert worker_result == [] + @pytest.mark.order(index=1) + def test_check_celery_workers_processing_tasks( + self, + celery_app: Celery, + sleep_sig: Signature, + launch_workers: "Fixture", # noqa: F821 + ): + """ + Test the check_celery_workers_processing function with workers active and a task in a queue. + This function will query workers for any tasks they're still processing. We'll send a + a task that sleeps for 3 seconds to our workers before we run this test so that there should be + a task for this function to find. + + NOTE: the celery app fixture shows strange behavior when using app.control.inspect() calls (which + check_celery_workers_processing uses) so we have to run this test first in this class in order to + have it run properly. + + :param celery_app: A pytest fixture for the test Celery app + :param sleep_sig: A pytest fixture for a celery signature of a task that sleeps for 3 sec + :param launch_workers: A pytest fixture that launches celery workers for us to interact with + """ + # Our active workers/queues are test_worker_[0-2]/test_queue_[0-2] so we're + # sending this to test_queue_0 for test_worker_0 to process + queue_for_signature = "test_queue_0" + sleep_sig.set(queue=queue_for_signature) + result = sleep_sig.delay() + + # We need to give the task we just sent to the server a second to get picked up by the worker + sleep(1) -class TestInactiveQueues: + # Run the test now that the task should be getting processed + active_queue_test = celeryadapter.check_celery_workers_processing([queue_for_signature], celery_app) + assert active_queue_test is True + + # Now test that a queue without any tasks returns false + # We sent the signature to task_queue_0 so task_queue_1 shouldn't have any tasks to find + non_active_queue_test = celeryadapter.check_celery_workers_processing(["test_queue_1"], celery_app) + assert non_active_queue_test is False + + # Wait for the worker to finish running the task + result.get() + + +class TestInactive: """ - This class will test queue related functions in the celeryadapter.py module. - It will run tests where we don't need any active queues to interact with. + This class will test functions in the celeryadapter.py module. + It will run tests where we don't need any active queues/workers to interact with. """ - def test_query_celery_queues(self): + def test_query_celery_queues(self, celery_app: Celery, worker_queue_map: Dict[str, str]): # noqa: F821 """ Test the query_celery_queues function by providing it with a list of inactive queues. - This should return a list of strings. Each string will give a message saying that a - particular queue was inactive + This should return a dict where keys are queue names and values are more dicts containing + the number of jobs and consumers in that queue (which should be 0 for both here). + + :param `celery_app`: A pytest fixture for the test Celery app + :param worker_queue_map: A pytest fixture that returns a dict of workers and queues """ - # TODO Modify query_celery_queues so the output for a redis broker is the same - # as the output for rabbit broker + # Set up a dummy configuration to use in the test + dummy_config = Config({"broker": {"name": "redis"}}) + + # Get the actual output + queues_to_query = list(worker_queue_map.values()) + actual_queue_info = celeryadapter.query_celery_queues(queues_to_query, app=celery_app, config=dummy_config) + + # Ensure all 3 queues in worker_queue_map were queried before looping + assert len(actual_queue_info) == 3 + + # Ensure each queue has no worker attached (since the queues should be inactive here) + for queue_name, queue_info in actual_queue_info.items(): + assert queue_name in worker_queue_map.values() + assert queue_info == {"consumers": 0, "jobs": 0} def test_celerize_queues(self, worker_queue_map: Dict[str, str]): """ @@ -144,17 +228,29 @@ def test_get_running_queues(self): Test the get_running_queues function with no queues active. This should return an empty list. """ - result = celeryadapter.get_running_queues("test_app", test_mode=True) + result = celeryadapter.get_running_queues("merlin_test_app", test_mode=True) assert result == [] - def test_get_queues(self, celery_app: celery.Celery): + def test_get_active_celery_queues(self, celery_app: Celery): """ - Test the get_queues function with no queues active. + Test the get_active_celery_queues function with no queues active. This should return a tuple where the first entry is an empty dict and the second entry is an empty list. :param `celery_app`: A pytest fixture for the test Celery app """ - queue_result, worker_result = celeryadapter.get_queues(celery_app) + queue_result, worker_result = celeryadapter.get_active_celery_queues(celery_app) assert queue_result == {} assert worker_result == [] + + def test_check_celery_workers_processing_tasks(self, celery_app: Celery, worker_queue_map: Dict[str, str]): + """ + Test the check_celery_workers_processing function with no workers active. + This function will query workers for any tasks they're still processing. Since no workers are active + this should return False. + + :param celery_app: A pytest fixture for the test Celery app + """ + # Run the test now that the task should be getting processed + result = celeryadapter.check_celery_workers_processing(list(worker_queue_map.values()), celery_app) + assert result is False From 642f925af5f83e5b250d4716a5404c8779318b02 Mon Sep 17 00:00:00 2001 From: Joe Koning Date: Fri, 19 Jan 2024 09:05:21 -0800 Subject: [PATCH 26/48] Add the missing restart keyword to the specification docs. (#459) --- CHANGELOG.md | 1 + docs/source/merlin_specification.rst | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0074e9d..9138821e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - The `merlin status` command so that it's consistent in its output whether using redis or rabbitmq as the broker - The `merlin monitor` command will now keep an allocation up if the queues are empty and workers are still processing tasks +- Add the restart keyword to the specification docs ## [1.11.1] ### Fixed diff --git a/docs/source/merlin_specification.rst b/docs/source/merlin_specification.rst index f857fabf3..71e041b33 100644 --- a/docs/source/merlin_specification.rst +++ b/docs/source/merlin_specification.rst @@ -120,6 +120,9 @@ see :doc:`./merlin_variables`. # The $(LAUNCHER) macro can be used to substitute a parallel launcher # based on the batch:type:. # It will use the nodes and procs values for the task. + # restart: The (optional) restart command to run when $(MERLIN_RESTART) + # is the exit code. The command in cmd will be run if the exit code + # is $(MERLIN_RETRY). # task_queue: the queue to assign the step to (optional. default: merlin) # shell: the shell to use for the command (eg /bin/bash /usr/bin/env python) # (optional. default: /bin/bash) @@ -156,6 +159,8 @@ see :doc:`./merlin_variables`. cmd: | cd $(runs1.workspace)/$(MERLIN_SAMPLE_PATH) + # exit $(MERLIN_RESTART) # syntax to send a restart error code + # This will rerun the cmd command. Users can also use $(MERLIN_RETRY). nodes: 1 procs: 1 depends: [runs1] @@ -167,7 +172,14 @@ see :doc:`./merlin_variables`. cmd: | touch learnrun.out $(LAUNCHER) echo "$(VAR1) $(VAR2)" >> learnrun.out - exit $(MERLIN_RETRY) # some syntax to send a retry error code + exit $(MERLIN_RESTART) # syntax to send a restart error code + # exit $(MERLIN_RETRY) # syntax to send a retry error code to + # run the cmd command again instead of the restart command. + restart: | + # Command to run if the $(MERLIN_RESTART) exit code is used + touch learnrunrs.out + $(LAUNCHER) echo "$(VAR1) $(VAR2)" >> learnrunrs.out + exit $(MERLIN_SUCCESS) # syntax to send a success code nodes: 1 procs: 1 task_queue: lqueue From 40930c226080ea225f74a0c0343cba1f6ce10d83 Mon Sep 17 00:00:00 2001 From: Brian Gunnarson <49216024+bgunnar5@users.noreply.github.com> Date: Thu, 25 Jan 2024 08:58:26 -0800 Subject: [PATCH 27/48] docs/conversion-to-mkdocs (#460) * remove a merge conflict statement that was missed * add base requirements for mkdocs * set up configuration for API docs * start work on porting user guide to mkdocs * add custom styling and contact page * begin work on porting tutorial to mkdocs * add new examples page * move old sphinx docs to their own folder (*delete later*) * modify some admonitions to be success * modify hello examples page and port step 3 of tutorial to mkdocs * fix typo in hello example * finish porting step 4 of tutorial to mkdocs * port part 5 of the tutorial to mkdocs * copy faq and contributing from old docs * port step 6 of tutorial to mkdocs * remove unused prereq * port step 7 of tutorial to mkdocs * add more detailed instructions on contributing * move venv page into installation and add spack instructions too * add configuration docs * add content to user guide landing page * port celery page to mkdocs * rearrange configuration pages to add in merlin server configuration instructions * port command line page to mkdocs * finish new landing page * change size of merlin logo * port variables page to mkdocs * fix broken links to configuration page * port FAQ to mkdocs * fix incorrect requirement name * update CHANGELOG * attempt to get docs to build through readthedocs * port docker page to mkdocs * port contributing guide to mkdocs * add new 'running studies' page * add path changes to images * add a page on how to interpret study output * add page on the spec file * remove old sphinx docs that are no longer needed * added README to docs and updated CHANGELOG * fix copyright and hello_samples tree * rearrange images/stylesheets and statements that use them * add suggestions from Luc and Joe * add tcsh instructions for venv activation * add Charle's suggestions for the landing page * change tcsh mentions to csh --- .readthedocs.yaml | 6 +- CHANGELOG.md | 15 + docs/Makefile | 31 - docs/README.md | 44 + docs/api_reference/index.md | 5 + docs/assets/images/hello-samples-tree.png | Bin 0 -> 124951 bytes .../basic-merlin-info-workspace.png | Bin 0 -> 28175 bytes .../basic-step-workspace.png | Bin 0 -> 27852 bytes .../merlin-info-with-samples.png | Bin 0 -> 48968 bytes .../modified-hierarchy-structure.png | Bin 0 -> 57958 bytes .../two-level-sample-hierarchy.png | Bin 0 -> 57043 bytes .../workspace-with-params-and-samples.png | Bin 0 -> 39547 bytes .../workspace-with-params.png | Bin 0 -> 97183 bytes .../workspace-with-samples.png | Bin 0 -> 37567 bytes docs/{ => assets}/images/merlin_arch.png | Bin .../images/merlin_banner.png} | Bin docs/assets/images/merlin_banner_white.png | Bin 0 -> 61782 bytes docs/{ => assets}/images/merlin_icon.png | Bin .../running_studies/current-node-launch.png | Bin 0 -> 46785 bytes .../running_studies/iterative-diagram.png | Bin 0 -> 106628 bytes .../running_studies/merlin-run-diagram.png | Bin 0 -> 120345 bytes .../running_studies/parallel-launch.png | Bin 0 -> 61759 bytes .../producer-consumer-model.png | Bin 0 -> 89461 bytes .../worker-server-communication.png | Bin 0 -> 60915 bytes .../advanced_topics/cumulative_results.png | Bin .../images/tutorial}/hello_world/dag1.png | Bin .../images/tutorial}/hello_world/dag2.png | Bin .../images/tutorial}/hello_world/dag3.png | Bin .../images/tutorial}/hello_world/dag4.png | Bin .../tutorial}/hello_world/merlin_output.png | Bin .../tutorial}/hello_world/merlin_output2.png | Bin .../introduction}/central_coordination.png | Bin .../introduction}/external_coordination.png | Bin .../introduction}/internal_coordination.png | Bin .../tutorial/introduction}/merlin_run.png | Bin .../introduction}/task_creation_rate.png | Bin .../run_simulation/lid-driven-stable.png | Bin .../tutorial}/run_simulation/openfoam_dag.png | Bin .../run_simulation/openfoam_wf_output.png | Bin .../tutorial}/run_simulation/prediction.png | Bin .../images/tutorial}/run_simulation/setup.png | Bin docs/assets/javascripts/swap_lp_image.js | 26 + docs/assets/stylesheets/extra.css | 8 + docs/contact.md | 21 + docs/examples/feature_demo.md | 3 + docs/examples/flux.md | 3 + docs/examples/hello.md | 734 +++++++++++ docs/examples/hpc.md | 3 + docs/examples/index.md | 48 + docs/examples/iterative.md | 3 + docs/examples/lsf.md | 3 + docs/examples/restart.md | 3 + docs/examples/slurm.md | 3 + docs/faq.md | 453 +++++++ docs/gen_ref_pages.py | 40 + docs/index.md | 226 ++++ docs/make.bat | 36 - docs/requirements.in | 4 - docs/requirements.txt | 10 +- docs/source/.gitignore | 4 - docs/source/_static/custom.css | 39 - docs/source/_static/custom.js | 25 - docs/source/app_config/app_amqp.yaml | 58 - docs/source/celery_overview.rst | 125 -- docs/source/conf.py | 183 --- docs/source/docker.rst | 93 -- docs/source/faq.rst | 476 -------- docs/source/getting_started.rst | 156 --- docs/source/index.rst | 83 -- docs/source/merlin_commands.rst | 480 -------- docs/source/merlin_config.rst | 303 ----- docs/source/merlin_developer.rst | 175 --- docs/source/merlin_server.rst | 72 -- docs/source/merlin_specification.rst | 358 ------ docs/source/merlin_variables.rst | 397 ------ docs/source/merlin_workflows.rst | 49 - .../advanced_topics/advanced_requirements.txt | 3 - .../advanced_topics/advanced_topics.rst | 415 ------- .../modules/advanced_topics/faker_demo.yaml | 116 -- docs/source/modules/before.rst | 47 - docs/source/modules/contribute.rst | 48 - docs/source/modules/hello_world/.gitignore | 1 - docs/source/modules/hello_world/celery.txt | 27 - docs/source/modules/hello_world/hello.yaml | 24 - .../modules/hello_world/hello_world.rst | 353 ------ docs/source/modules/hello_world/local_out.txt | 31 - docs/source/modules/hello_world/run_out.txt | 25 - .../modules/hello_world/run_workers_out.txt | 21 - .../modules/hello_world/stop_workers.txt | 24 - .../installation/app_docker_rabbit.yaml | 10 - .../installation/app_docker_redis.yaml | 11 - .../modules/installation/app_local_redis.yaml | 11 - .../modules/installation/docker-compose.yml | 23 - .../installation/docker-compose_rabbit.yml | 46 - .../docker-compose_rabbit_redis_tls.yml | 38 - .../modules/installation/installation.rst | 340 ------ docs/source/modules/introduction.rst | 476 -------- docs/source/modules/port_your_application.rst | 70 -- .../modules/run_simulation/run_simulation.rst | 429 ------- docs/source/server/commands.rst | 94 -- docs/source/server/configuration.rst | 75 -- docs/source/spack.rst | 131 -- docs/source/virtualenv.rst | 54 - docs/tutorial/0_prerequisites.md | 36 + docs/tutorial/1_introduction.md | 242 ++++ docs/tutorial/2_installation.md | 516 ++++++++ docs/tutorial/3_hello_world.md | 478 ++++++++ docs/tutorial/4_run_simulation.md | 331 +++++ docs/tutorial/5_advanced_topics.md | 430 +++++++ docs/tutorial/6_contribute.md | 112 ++ docs/tutorial/7_port_application.md | 57 + .../tutorial.rst => tutorial/index.md} | 30 +- docs/user_guide/celery.md | 205 ++++ docs/user_guide/command_line.md | 684 +++++++++++ .../configuration/external_server.md | 407 +++++++ docs/user_guide/configuration/index.md | 266 ++++ .../user_guide/configuration/merlin_server.md | 177 +++ docs/user_guide/contributing.md | 193 +++ docs/user_guide/docker.md | 332 +++++ docs/user_guide/index.md | 23 + docs/user_guide/installation.md | 271 +++++ docs/user_guide/interpreting_output.md | 1072 +++++++++++++++++ docs/user_guide/running_studies.md | 520 ++++++++ docs/user_guide/specification.md | 725 +++++++++++ docs/user_guide/variables.md | 254 ++++ merlin/main.py | 2 +- mkdocs.yml | 126 ++ 127 files changed, 9115 insertions(+), 6121 deletions(-) delete mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/api_reference/index.md create mode 100644 docs/assets/images/hello-samples-tree.png create mode 100644 docs/assets/images/interpreting_output/basic-merlin-info-workspace.png create mode 100644 docs/assets/images/interpreting_output/basic-step-workspace.png create mode 100644 docs/assets/images/interpreting_output/merlin-info-with-samples.png create mode 100644 docs/assets/images/interpreting_output/modified-hierarchy-structure.png create mode 100644 docs/assets/images/interpreting_output/two-level-sample-hierarchy.png create mode 100644 docs/assets/images/interpreting_output/workspace-with-params-and-samples.png create mode 100644 docs/assets/images/interpreting_output/workspace-with-params.png create mode 100644 docs/assets/images/interpreting_output/workspace-with-samples.png rename docs/{ => assets}/images/merlin_arch.png (100%) rename docs/{images/merlin.png => assets/images/merlin_banner.png} (100%) create mode 100644 docs/assets/images/merlin_banner_white.png rename docs/{ => assets}/images/merlin_icon.png (100%) create mode 100644 docs/assets/images/running_studies/current-node-launch.png create mode 100644 docs/assets/images/running_studies/iterative-diagram.png create mode 100644 docs/assets/images/running_studies/merlin-run-diagram.png create mode 100644 docs/assets/images/running_studies/parallel-launch.png create mode 100644 docs/assets/images/running_studies/producer-consumer-model.png create mode 100644 docs/assets/images/running_studies/worker-server-communication.png rename docs/{source/modules => assets/images/tutorial}/advanced_topics/cumulative_results.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/dag1.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/dag2.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/dag3.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/dag4.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/merlin_output.png (100%) rename docs/{source/modules => assets/images/tutorial}/hello_world/merlin_output2.png (100%) rename docs/{images => assets/images/tutorial/introduction}/central_coordination.png (100%) rename docs/{images => assets/images/tutorial/introduction}/external_coordination.png (100%) rename docs/{images => assets/images/tutorial/introduction}/internal_coordination.png (100%) rename docs/{images => assets/images/tutorial/introduction}/merlin_run.png (100%) rename docs/{images => assets/images/tutorial/introduction}/task_creation_rate.png (100%) rename docs/{source/modules => assets/images/tutorial}/run_simulation/lid-driven-stable.png (100%) rename docs/{source/modules => assets/images/tutorial}/run_simulation/openfoam_dag.png (100%) rename docs/{source/modules => assets/images/tutorial}/run_simulation/openfoam_wf_output.png (100%) rename docs/{source/modules => assets/images/tutorial}/run_simulation/prediction.png (100%) rename docs/{source/modules => assets/images/tutorial}/run_simulation/setup.png (100%) create mode 100644 docs/assets/javascripts/swap_lp_image.js create mode 100644 docs/assets/stylesheets/extra.css create mode 100644 docs/contact.md create mode 100644 docs/examples/feature_demo.md create mode 100644 docs/examples/flux.md create mode 100644 docs/examples/hello.md create mode 100644 docs/examples/hpc.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/iterative.md create mode 100644 docs/examples/lsf.md create mode 100644 docs/examples/restart.md create mode 100644 docs/examples/slurm.md create mode 100644 docs/faq.md create mode 100644 docs/gen_ref_pages.py create mode 100644 docs/index.md delete mode 100644 docs/make.bat delete mode 100644 docs/requirements.in delete mode 100644 docs/source/.gitignore delete mode 100644 docs/source/_static/custom.css delete mode 100644 docs/source/_static/custom.js delete mode 100644 docs/source/app_config/app_amqp.yaml delete mode 100644 docs/source/celery_overview.rst delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/docker.rst delete mode 100644 docs/source/faq.rst delete mode 100644 docs/source/getting_started.rst delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/merlin_commands.rst delete mode 100644 docs/source/merlin_config.rst delete mode 100644 docs/source/merlin_developer.rst delete mode 100644 docs/source/merlin_server.rst delete mode 100644 docs/source/merlin_specification.rst delete mode 100644 docs/source/merlin_variables.rst delete mode 100644 docs/source/merlin_workflows.rst delete mode 100644 docs/source/modules/advanced_topics/advanced_requirements.txt delete mode 100644 docs/source/modules/advanced_topics/advanced_topics.rst delete mode 100644 docs/source/modules/advanced_topics/faker_demo.yaml delete mode 100644 docs/source/modules/before.rst delete mode 100644 docs/source/modules/contribute.rst delete mode 100644 docs/source/modules/hello_world/.gitignore delete mode 100644 docs/source/modules/hello_world/celery.txt delete mode 100644 docs/source/modules/hello_world/hello.yaml delete mode 100644 docs/source/modules/hello_world/hello_world.rst delete mode 100644 docs/source/modules/hello_world/local_out.txt delete mode 100644 docs/source/modules/hello_world/run_out.txt delete mode 100644 docs/source/modules/hello_world/run_workers_out.txt delete mode 100644 docs/source/modules/hello_world/stop_workers.txt delete mode 100644 docs/source/modules/installation/app_docker_rabbit.yaml delete mode 100644 docs/source/modules/installation/app_docker_redis.yaml delete mode 100644 docs/source/modules/installation/app_local_redis.yaml delete mode 100644 docs/source/modules/installation/docker-compose.yml delete mode 100644 docs/source/modules/installation/docker-compose_rabbit.yml delete mode 100644 docs/source/modules/installation/docker-compose_rabbit_redis_tls.yml delete mode 100644 docs/source/modules/installation/installation.rst delete mode 100644 docs/source/modules/introduction.rst delete mode 100644 docs/source/modules/port_your_application.rst delete mode 100644 docs/source/modules/run_simulation/run_simulation.rst delete mode 100644 docs/source/server/commands.rst delete mode 100644 docs/source/server/configuration.rst delete mode 100644 docs/source/spack.rst delete mode 100644 docs/source/virtualenv.rst create mode 100644 docs/tutorial/0_prerequisites.md create mode 100644 docs/tutorial/1_introduction.md create mode 100644 docs/tutorial/2_installation.md create mode 100644 docs/tutorial/3_hello_world.md create mode 100644 docs/tutorial/4_run_simulation.md create mode 100644 docs/tutorial/5_advanced_topics.md create mode 100644 docs/tutorial/6_contribute.md create mode 100644 docs/tutorial/7_port_application.md rename docs/{source/tutorial.rst => tutorial/index.md} (51%) create mode 100644 docs/user_guide/celery.md create mode 100644 docs/user_guide/command_line.md create mode 100644 docs/user_guide/configuration/external_server.md create mode 100644 docs/user_guide/configuration/index.md create mode 100644 docs/user_guide/configuration/merlin_server.md create mode 100644 docs/user_guide/contributing.md create mode 100644 docs/user_guide/docker.md create mode 100644 docs/user_guide/index.md create mode 100644 docs/user_guide/installation.md create mode 100644 docs/user_guide/interpreting_output.md create mode 100644 docs/user_guide/running_studies.md create mode 100644 docs/user_guide/specification.md create mode 100644 docs/user_guide/variables.md create mode 100644 mkdocs.yml diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ee62bcb5e..c3cfbbe07 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,11 +5,11 @@ build: tools: python: "3.8" -sphinx: - configuration: docs/source/conf.py - python: install: - requirements: docs/requirements.txt +mkdocs: + fail_on_warning: false + formats: [pdf] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9138821e2..281d3b068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `merlin monitor` command will now keep an allocation up if the queues are empty and workers are still processing tasks - Add the restart keyword to the specification docs +### Changed +- The entire documentation has been ported to MkDocs and re-organized + - *Dark Mode* + - New "Getting Started" example for a simple setup tutorial + - More detail on configuration instructions + - There's now a full page on installation instructions + - More detail on explaining the spec file + - More detail with the CLI page + - New "Running Studies" page to explain different ways to run studies, restart them, and accomplish command line substitution + - New "Interpreting Output" page to help users understand how the output workspace is generated in more detail + - New "Examples" page has been added + - Updated "FAQ" page to include more links to helpful locations throughout the documentation + - Set up a place to store API docs + - New "Contact" page with info on reaching Merlin devs + ## [1.11.1] ### Fixed - Typo in `batch.py` that caused lsf launches to fail (`ALL_SGPUS` changed to `ALL_GPUS`) diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 662696c6f..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = Merlin -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -code-docs: - sphinx-apidoc -f -o source/ ../merlin/ - -view: code-docs html - firefox -new-instance build/html/index.html - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - pip install -r requirements.txt - echo $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -clean: - rm -rf build/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..cbddc54a4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,44 @@ +# Guide to Merlin Documentation + +Merlin uses [MkDocs](https://www.mkdocs.org/) to generate its documentation and [Read the Docs](https://about.readthedocs.com/?ref=readthedocs.com) to host it. This README will detail important information on handling the documentation. + +## How to Build the Documentation + +Ensure you're at the root of the Merlin repository: + +```bash +cd /path/to/merlin/ +``` + +Install the documentation with: + +```bash +pip install -r docs/requirements.txt +``` + +Build the documentation with: + +```bash +mkdocs serve +``` + +Once up and running, MkDocs should provide a message telling you where your browser is connected (this is typically `http://127.0.0.1:8000/`). If you're using VSCode, you should be able to `ctrl+click` on the address to open the browser window. An example is shown below: + +```bash +(venv_name) [user@machine:merlin]$ mkdocs serve +INFO - Building documentation... +INFO - Cleaning site directory +WARNING - Excluding 'README.md' from the site because it conflicts with 'index.md'. +WARNING - A relative path to 'api_reference/' is included in the 'nav' configuration, which is not found in the documentation files. +INFO - Documentation built in 3.24 seconds +INFO - [09:16:00] Watching paths for changes: 'docs', 'mkdocs.yml' +INFO - [09:16:00] Serving on http://127.0.0.1:8000/ +``` + +## Configuring the Documentation + +MkDocs relies on an `mkdocs.yml` file for almost everything to do with configuration. See [their Configuration documentation](https://www.mkdocs.org/user-guide/configuration/) for more information. + +## How Do API Docs Work? + +Coming soon... diff --git a/docs/api_reference/index.md b/docs/api_reference/index.md new file mode 100644 index 000000000..6707457de --- /dev/null +++ b/docs/api_reference/index.md @@ -0,0 +1,5 @@ +# Merlin API Reference + +Coming soon! + + diff --git a/docs/assets/images/hello-samples-tree.png b/docs/assets/images/hello-samples-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..8893874bfea5f52d7d04c0e123e6a9ec8cca352a GIT binary patch literal 124951 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%0Dh9eAw7nDOAr}znprA`_huh%llIq z8s!{c`e?$;u}T*CeFbjBjBEv=f3WqHo|esum#ZFS1-liqsrMJ>Ig%z>RZP)yj&}sD zYUsEbEx#~}-RDTU+WP2A#i+)O7sz3WQX9##B}0Au+iB1sfOwQ_^|bj)wpLUbBQuMs z(dOsusLod8?AD)(^ovI3UUnuC8kWV>LUqN=GXxgs7 z$v!b60OhqfAT_}peB<&pZqRu839BWg!NdkPF#=sP5m!HfdwB&Y&jiWc&buQXzwu8O zO4ttL=WYvOs$wMgPOrT|e21L}&b4g8<)^=#B;EZfd6wPY+dJ|U^X;AYW=d{Vy)2zB z%$&SA1CRIp@?AD&vwMe&J}*lbzfMX^=`UG=rP`$Jjzjd=%Vi;`D0Zl+V<5b#%Ij{aR2 z>*wD+K6VJT-J^bCRw*YZC$|;2vYF@7SbL5NTe^tm|+*eLeSBQhmH2N|Yvaj<3IK*Q;7 z$D+~X*6O4I%`XeUb{KW1m|1h^_Dmy|Tp2db;2~P^3p6nFP5;``|-Ruy#WIZ$Ph= zjMdiBF|=Zty$Tt+fheL+Av$%}@j1dh4IE{^cW0tNVXeNS59|^aT9T6&sK7DpOra>y zgy^RFcp5x?R@zxNP*EMN_SdnJSX(K5MD)s{=|_-y^BF@Ek3ZjZ|dpG7^rLBmmq@yG*;tZWhC--LQUzNnDvC zW@fRvX*Yl3NYP!8Fc`4z;jiVX{iG&--v^IqWvG-T89X=GC}3daUqoUfSl84*9-?^7 z+qQACeFY{H%WK`o9x|>vbP0^I@nhGuty7Yc(gVZ-%uuG#jR4kE)%ynqNuwT&P{T|E zIg=xK5$fj4>|}wHx?lf8orAVNnLbXNG)8Xp1(X?&@99r%Z7YozV*wi;us6FBl5*c= zp(6Js=myRavo+&1Gr9~8Nri+9m6!YOPEeSP_xe<>)yM_h7L089?b*`uNcQ2@rBiWJ zI7!S(+BETnD`uWL{wblN%lIg_002lA*-gs%;kT`!L z+D_Epno!@U05=BsGWLgW0T)IPX*Cy`odSaFA}<97eee2Do0V{qYQMA60YI2S1yucp zuNOK>(EHa>ZnAcYO=-PH;w&zgIT*MOdp+@fI!>{~lvMi@bVHa#f3zAABD+Ngls1he z4Wkpub4($|FeSb zz!J?5clf#z%a)*EeLgXEtm9!$>DuOLMW4s59enE=iSu{LdIub9JYW4aViV40$R6kzd~aFj`}k;5R>Ig0wH`=cJIiUae1XMTEs|ClF+xO_OVvs z(_`M9Ef#t~d_9h47x6LH)#74dK_D0UV*2SXT|>ghTZjhl_N3P`t?r7hwn?HEZJj5s zJ8~`A)g<_V;bb#XYhNp=yWW;6k}R)<#YJ1W5~U~s%nG1 zlg^FMtj2MQP$ZVz?!v`vy*N_rGWPZSMCZ(UtK3fWEWhu=03Rd?H!=%i2ZNJwb}YGK z+<@E{1tI=|y>PAu*)F3721YAeYKmYZaB=(4%`7oMBve5Tg;&Mzi~MONh?prBs+SRT zvK|^ZrGIr3gfs7a3cRXv>Tk?5YH=iEJzUOFs>RAcwUc0P3;F4x|oMbS;~HGw%emYbcaFTD5< zp@_Gn%){pr#MHy9mQzx^!O)#w=nq;|+z6{+&(d4nf81>c!6FidJvkZWrk}icvN0WM zH8h+_tV{)AD`&m&a(Qcc$K5dqzh)QkHz8!xNT^Xl;n=K0-3CLD#WHmv&aTvsHx}LE zSJA|3LWrbKU=MEQ{_I;N?)3hct(Hq>^Odus)uEEXL;ohwuU!aZAh!`^!x)?mEVt^p za-(SVyiW!fd=O(vq&*~al@`LI!1Rs;kiRxcn-!jj*c%-Xr+seaHRd}@uH7tVH$!eT zD6qa>|0O%CjtWrT7+8zgbRtmRKU=YibLE^zb@!o#;NZ5(p76Z9EfkI=FMtZdYR{%5 z5t&h6$3b5n!kQGOAR|)YR+rk;hAXeK2c@HuHZS=o5B1Q>i+IEZ6DsWF+^q-r2L7hfo65DNSWCNzhEsPG&=jLHAB7R0boO>N* z1XmzV&0g1FJP@PB^;YI3?i-mpi()~r?`WdWL+=DWY`Syl|6nB-J zklWr%BVEYr4#-zGefZ5L-*Z?>6-;#a38q!Dh}a}|KLH6fA)Ef2!4vnno@#4rznN@` zHX;B9QPlH4AW>|b1~{w@CKr-d=gT%8v<`6dxFm_^q0I-JQjSP4UG8!OjHmL*D z!7sD7}-Z4O<0vPPsG*yOeo_YCi zE?@dLf7@tawDo7MI};p;*$9{qk&ESmlb`rcTVX~P#vqGb=>LYYcmYlUUc1BrN+VDa z86>0rgeZm{PvTqy#7!XW*r?rs$=Y?`tGyNLTF=FEhOUT=*4B*m7(bsGyqXDk@G+u> zp|75hog^fn{EWQ7jSmMkS=4vW_MNv_F7Vx%KxxNPs$ttUdA6ZKf#J3F2uSx%+5$V* z-J{C-ni9A{*5F21fSE7(U=l}>6rj`>V}gjYjg~`(2ZaIWmu))($1K4Wgqd3)O)X?V zOp>4zyqd@eka^DCDhEFeNag8CnY4j4UqrnN{x2;q*H};&VB$O73K-cDKJ#;LDB_vd zfFVY2hDipzmkfwC5xDPz0^-wcz$I`{s6c9P-@kt!tX$I|+$AMW+mmye7UAc4C~OrB zTS8GTfYrvWwvAHP!a?<-#+x9?p&0>!bmGN=elR^_RGw(#Z|DhW-lz43)q>d76d-iq zuxlp4$wr2A4CV}!CmaaLYIy&pQ->KlnJMZ%bqmz&|rd^E7$HS#@>!tXk}bZDmtQ>0=yN?5eA#DI_*&?q6=K!j=h z&(7Ap7YH!gN+h%EI0yVdpi8*5K|o-f$Lu{)f2FgBIQ zd1}hr7I=nsettM0vPBD^dEypOM|f-Z)MkZsQ@(mfeqC5C1s8xR(!sJV4A;RW>toQA zEmf@#fXDri{ApOOh)hJlr5(3&Sa3G&LqYUp_1Z%Kc-ma8Gj3I(l06egp@g1eR-R#i z&m2}Qux8CgW3HYB-*pF6Ox$@vK@dW{1-9OB_Ugj&bD$r2#o!-KrQWRfmxU9wU=QJ@ zDC0L+*5_;AV!>?`d$V#dy$EJ#=Pad$8A9tGpZe)jf);!AM*>kjAL zO%e&Yyzu)l_&$$$g>(??E6k>Qkl@Gt8&ux_@lxL4*98*+;Ysv8r(n&7j!fL0}M)Lu;Xvnk{``z*}L{M z29$66wZcb}g(5>GrJ1u}wMY~VD+D@c8V<3E=S4*FJOt+l4nraRXspWY9}gXv3k>3h zhnb6u3j75|6cko%ba7U_Z~*~il7PihkdgUS={|IVR9xMMH?7!dY_&