diff --git a/errors/errors.yml b/errors/errors.yml index b9744ce..b0b3130 100644 --- a/errors/errors.yml +++ b/errors/errors.yml @@ -1,7 +1,7 @@ # Task parsing errors (error code 1xxx) 1000: name: TaskParseError - message: "An unknown error occurred when parsing a COND file." + message: "An error occurred when parsing a COND file or a file that it includes: {error_details}." 1001: name: MissingTaskParameter @@ -29,7 +29,8 @@ 1007: name: TaskSyntaxError - message: "Encountered a syntax error when parsing a COND file." + message: >- + Encountered a syntax error when parsing a COND file or a file that it includes. 1008: name: ParsingUnknownNameError @@ -87,6 +88,12 @@ All arguments must be either a string, integer, floating point number, or boolean. +1017: + name: IncludeFileInvalidExtension + message: >- + Encountered an include() of '{included_file}', which does not have a '.cond' + extension. Conductor only supports including .cond files. + # Task graph loading errors (error code 2xxx) 2001: @@ -105,6 +112,14 @@ name: UnsupportedVersionIndexFormat message: "Detected an unsupported version index ({version}). Please make sure that you are using the latest version of Conductor." +2005: + name: IncludeFileNotFound + message: Encountered an include() of '{included_file}'. However, that file does not exist. + +2006: + name: IncludeFileNotInProject + message: Encountered an include() of '{included_file}'. However, that file is not inside the project. + # Execution errors (error code 3xxx) 3001: diff --git a/pytest.ini b/pytest.ini index 768b6b1..9c5504d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] # Configuration option for `pytest-timeout` -# An individual unit test cannot take longer than 30 seconds. -timeout = 30 +# An individual unit test cannot take longer than 60 seconds. +timeout = 60 diff --git a/src/conductor/config.py b/src/conductor/config.py index eb5c24b..eede451 100644 --- a/src/conductor/config.py +++ b/src/conductor/config.py @@ -62,3 +62,6 @@ # The name of the experiment arguments serialized JSON file. EXP_ARGS_JSON_FILE_NAME = "args.json" + +# COND files can only include files with this extension. +COND_INCLUDE_EXTENSION = ".cond" diff --git a/src/conductor/errors/base.py b/src/conductor/errors/base.py index 0cb9681..5d178b9 100644 --- a/src/conductor/errors/base.py +++ b/src/conductor/errors/base.py @@ -17,6 +17,11 @@ def add_file_context(self, file_path, line_number=None): self.file_context = FileContext(file_path, line_number) return self + def add_file_context_if_missing(self, file_path, line_number=None): + if self.file_context is not None: + return self + return self.add_file_context(file_path, line_number) + def add_extra_context(self, context_string): self.extra_context = context_string return self diff --git a/src/conductor/errors/generated.py b/src/conductor/errors/generated.py index b7d439e..8eb8363 100644 --- a/src/conductor/errors/generated.py +++ b/src/conductor/errors/generated.py @@ -11,11 +11,11 @@ class TaskParseError(ConductorError): def __init__(self, **kwargs): super().__init__() - + self.error_details = kwargs["error_details"] def _message(self): - return "An unknown error occurred when parsing a COND file.".format( - + return "An error occurred when parsing a COND file or a file that it includes: {error_details}.".format( + error_details=self.error_details, ) @@ -111,7 +111,7 @@ def __init__(self, **kwargs): def _message(self): - return "Encountered a syntax error when parsing a COND file.".format( + return "Encountered a syntax error when parsing a COND file or a file that it includes.".format( ) @@ -241,6 +241,19 @@ def _message(self): ) +class IncludeFileInvalidExtension(ConductorError): + error_code = 1017 + + def __init__(self, **kwargs): + super().__init__() + self.included_file = kwargs["included_file"] + + def _message(self): + return "Encountered an include() of '{included_file}', which does not have a '.cond' extension. Conductor only supports including .cond files.".format( + included_file=self.included_file, + ) + + class TaskNotFound(ConductorError): error_code = 2001 @@ -293,6 +306,32 @@ def _message(self): ) +class IncludeFileNotFound(ConductorError): + error_code = 2005 + + def __init__(self, **kwargs): + super().__init__() + self.included_file = kwargs["included_file"] + + def _message(self): + return "Encountered an include() of '{included_file}'. However, that file does not exist.".format( + included_file=self.included_file, + ) + + +class IncludeFileNotInProject(ConductorError): + error_code = 2006 + + def __init__(self, **kwargs): + super().__init__() + self.included_file = kwargs["included_file"] + + def _message(self): + return "Encountered an include() of '{included_file}'. However, that file is not inside the project.".format( + included_file=self.included_file, + ) + + class TaskNonZeroExit(ConductorError): error_code = 3001 @@ -586,10 +625,13 @@ def _message(self): "ExperimentGroupDuplicateName", "ExperimentGroupInvalidExperimentInstance", "RunArgumentsNonPrimitiveValue", + "IncludeFileInvalidExtension", "TaskNotFound", "MissingProjectRoot", "CyclicDependency", "UnsupportedVersionIndexFormat", + "IncludeFileNotFound", + "IncludeFileNotInProject", "TaskNonZeroExit", "TaskFailed", "OutputDirTaken", diff --git a/src/conductor/parsing/task_index.py b/src/conductor/parsing/task_index.py index ffe59df..4901a58 100644 --- a/src/conductor/parsing/task_index.py +++ b/src/conductor/parsing/task_index.py @@ -16,7 +16,7 @@ class TaskIndex: def __init__(self, project_root: pathlib.Path): self._project_root = project_root - self._task_loader = TaskLoader() + self._task_loader = TaskLoader(project_root) # Keyed by the relative path to the COND file self._loaded_raw_tasks: Dict[pathlib.Path, Dict[str, Dict]] = {} # Keyed by task identifier diff --git a/src/conductor/parsing/task_loader.py b/src/conductor/parsing/task_loader.py index 1a6bafa..cb01674 100644 --- a/src/conductor/parsing/task_loader.py +++ b/src/conductor/parsing/task_loader.py @@ -1,3 +1,6 @@ +import pathlib +from typing import Any, Dict, Optional +from conductor.config import COND_INCLUDE_EXTENSION from conductor.task_types import raw_task_types from conductor.errors import ( ConductorError, @@ -5,53 +8,80 @@ MissingCondFile, ParsingUnknownNameError, TaskSyntaxError, + TaskParseError, + IncludeFileInvalidExtension, + IncludeFileNotFound, + IncludeFileNotInProject, ) from conductor.task_types.stdlib import STDLIB_FILES class TaskLoader: - def __init__(self): - self._tasks = None - self._current_cond_file_path = None + def __init__(self, project_root: pathlib.Path): + self._project_root = project_root + self._tasks: Optional[Dict[str, Dict]] = None + self._current_cond_file_path: Optional[pathlib.Path] = None self._conductor_scope = self._compile_scope() + self._curr_exec_scope = None - def parse_cond_file(self, cond_file_path): + # We cache the results from evaluating `include()`s so that if a file is + # included across multiple `COND` files, we do not repeatedly evaluate + # the file. The code being included is expected to be deterministic. + # + # Key is the absolute path to the file (e.g. /home/user/path/to/file.cond). + # Value is the resulting scope object. + self._include_cache: Dict[str, Any] = {} + + def parse_cond_file(self, cond_file_path: pathlib.Path): """ Parses all the tasks in a single COND file. """ - tasks = {} + tasks: Dict[str, Dict] = {} self._tasks = tasks self._current_cond_file_path = cond_file_path try: with open(cond_file_path, encoding="UTF-8") as file: code = file.read() + self._curr_exec_scope = self._conductor_scope.copy() # pylint: disable=exec-used - exec(code, self._conductor_scope.copy()) + exec(code, self._curr_exec_scope) return tasks except ConductorError as ex: - ex.add_file_context(file_path=cond_file_path) + ex.add_file_context_if_missing( + file_path=self._to_project_path(cond_file_path) + ) raise ex except SyntaxError as ex: syntax_err = TaskSyntaxError() syntax_err.add_file_context( - file_path=cond_file_path, + file_path=self._to_project_path(cond_file_path), line_number=ex.lineno, ) raise syntax_err from ex except NameError as ex: name_err = ParsingUnknownNameError(error_message=str(ex)) - name_err.add_file_context(file_path=cond_file_path) + name_err.add_file_context(file_path=self._to_project_path(cond_file_path)) raise name_err from ex except FileNotFoundError as ex: missing_file_err = MissingCondFile() - missing_file_err.add_file_context(file_path=cond_file_path) + missing_file_err.add_file_context( + file_path=self._to_project_path(cond_file_path) + ) raise missing_file_err from ex + except Exception as ex: + run_err = TaskParseError(error_details=str(ex)) + run_err.add_file_context(file_path=self._to_project_path(cond_file_path)) + raise run_err from ex finally: self._tasks = None self._current_cond_file_path = None + self._curr_exec_scope = None def _compile_scope(self): - scope = {} + scope = { + # Used to handle included files. + "include": self._run_include, + } # Create the task constructors for Conductor's foundational task types. for raw_task_type in raw_task_types.values(): scope[raw_task_type.name] = self._wrap_task_function( @@ -76,3 +106,77 @@ def shim(**kwargs): self._tasks[raw_task["name"]] = raw_task return shim + + def _run_include(self, candidate_path: str): + assert self._current_cond_file_path is not None + assert self._curr_exec_scope is not None + + # 1. Validate `candidate_path`. + if not candidate_path.endswith(COND_INCLUDE_EXTENSION): + raise IncludeFileInvalidExtension(included_file=candidate_path) + + # 2. Parse `candidate_path`. + if candidate_path.startswith("//"): + include_path = self._project_root.joinpath(candidate_path[2:]) + else: + include_path = self._current_cond_file_path.parent.joinpath(candidate_path) + try: + include_path = include_path.resolve(strict=True) + except FileNotFoundError as ex: + raise IncludeFileNotFound(included_file=candidate_path) from ex + + # 3. Make sure `include_path` is inside our project. + # If `include_path` is not relative to `self._project_root` then the + # method will raise a `ValueError`. For compatibility with Python 3.8, + # we do not use `is_relative_to()` (it is a Python 3.9+ method). + try: + include_path.relative_to(self._project_root) + except ValueError as ex: + raise IncludeFileNotInProject(included_file=candidate_path) from ex + + # 4. Check if the file is in our cache. If so, just use the cached results. + if str(include_path) in self._include_cache: + self._curr_exec_scope.update(self._include_cache[str(include_path)]) + return + + # 5. Run the included file. We purposely use a separate scope so that + # the Conductor task symbols (e.g., run_experiment()) are not available + # in the included file. + with open(include_path, encoding="UTF-8") as file: + include_code = file.read() + scope: Dict[str, Any] = {} + try: + # pylint: disable=exec-used + exec(include_code, {}, scope) + except SyntaxError as ex: + syntax_err = TaskSyntaxError() + syntax_err.add_file_context( + file_path=self._to_project_path(include_path), + line_number=ex.lineno, + ).add_extra_context( + "This error occurred while parsing a file included by {}.".format( + self._to_project_path(self._current_cond_file_path) + ) + ) + raise syntax_err from ex + except Exception as ex: + run_err = TaskParseError(error_details=str(ex)) + run_err.add_file_context( + file_path=self._to_project_path(include_path) + ).add_extra_context( + "This error occurred while parsing a file included by {}.".format( + self._to_project_path(self._current_cond_file_path) + ) + ) + raise run_err from ex + + # 6. Update the current scope with the new symbols. + self._curr_exec_scope.update(scope) + + # 7. Update the cache. + self._curr_exec_scope[str(include_path)] = scope + + def _to_project_path(self, path: pathlib.Path) -> str: + """Converts the given path to a path that is relative to the project root.""" + rel_path = path.relative_to(self._project_root) + return "//{}".format(rel_path) diff --git a/tests/cond_run_include_test.py b/tests/cond_run_include_test.py new file mode 100644 index 0000000..52eadb0 --- /dev/null +++ b/tests/cond_run_include_test.py @@ -0,0 +1,101 @@ +import pathlib + +from .conductor_runner import ConductorRunner, FIXTURE_TEMPLATES + +# Scenarios where include() is used incorrectly. + + +def test_cond_include_badext(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # Includes a file that does not have a ".cond" extension. + res = cond.run("//errors/badext:main") + assert res.returncode != 0 + + +def test_cond_include_nonexistent_relative(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # Includes a file using a relative path that does not exist. + res = cond.run("//errors/nonexistent:main") + assert res.returncode != 0 + + +def test_cond_include_nonexistent_project_relative(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # Includes a file using a project-relative path (e.g. + # //path/to/include.cond) that does not exist. + res = cond.run("//errors/nonexistent2:main") + assert res.returncode != 0 + + +def test_cond_include_external(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # Includes a file that exists but is not in this project. + res = cond.run("//errors/outside-project:main") + assert res.returncode != 0 + + +def test_cond_include_nested(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # The included file also tries to include a file. + res = cond.run("//errors/nested:main") + assert res.returncode != 0 + + +def test_cond_include_define_task(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + # The included file tries to define a task (this is not allowed). + res = cond.run("//errors/defines-tasks:main") + assert res.returncode != 0 + + +# Scenarios where include() is used correctly. + + +def check_output_file( + out_dir: pathlib.Path, expected_str: str, expected_value: int = (123 + 1337) +): + # `expected_value` is the sum of `VALUE1` and `VALUE2`, which are defined in + # the included `.cond` files. + with open(out_dir / "out.txt", encoding="UTF-8") as file: + contents = file.read().strip() + parts = contents.split("-") + assert len(parts) == 2 + assert parts[0] == expected_str + assert int(parts[1]) == expected_value + + +def test_cond_include_exp_separate(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + + res = cond.run("//sharing/exp1:exp1") + assert res.returncode == 0 + exp1_out = cond.find_task_output_dir("//sharing/exp1:exp1", is_experiment=True) + assert exp1_out is not None + check_output_file(exp1_out, "exp1") + + res = cond.run("//sharing/exp2:exp2") + assert res.returncode == 0 + exp2_out = cond.find_task_output_dir("//sharing/exp2:exp2", is_experiment=True) + assert exp2_out is not None + check_output_file(exp2_out, "exp2") + + +def test_cond_include_exp_combined(tmp_path: pathlib.Path): + cond = ConductorRunner.from_template(tmp_path, FIXTURE_TEMPLATES["include"]) + + # This should exercise the include caching codepath. Both dependencies of + # `//sharing:combine` include the same files. + res = cond.run("//sharing:both") + assert res.returncode == 0 + + # Sanity check. + both_out = cond.find_task_output_dir("//sharing:both", is_experiment=False) + assert both_out is not None + + exp1_out = cond.find_task_output_dir("//sharing/exp1:exp1", is_experiment=True) + assert exp1_out is not None + check_output_file(exp1_out, "exp1") + + exp2_out = cond.find_task_output_dir("//sharing/exp2:exp2", is_experiment=True) + assert exp2_out is not None + check_output_file(exp2_out, "exp2") diff --git a/tests/conductor_runner.py b/tests/conductor_runner.py index a5cc4a3..f0fd92a 100644 --- a/tests/conductor_runner.py +++ b/tests/conductor_runner.py @@ -176,4 +176,5 @@ def count_task_outputs(in_dir: pathlib.Path): "lib-test": pathlib.Path(_TESTS_DIR, "fixture-projects", "lib-test"), "git-context": pathlib.Path(_TESTS_DIR, "fixture-projects", "git-context"), "git-commit": pathlib.Path(_TESTS_DIR, "fixture-projects", "git-commit"), + "include": pathlib.Path(_TESTS_DIR, "fixture-projects", "include"), } diff --git a/tests/fixture-projects/experiments/include_test.cond b/tests/fixture-projects/experiments/include_test.cond new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixture-projects/include/cond_config.toml b/tests/fixture-projects/include/cond_config.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixture-projects/include/errors/badext/COND b/tests/fixture-projects/include/errors/badext/COND new file mode 100644 index 0000000..28d1d55 --- /dev/null +++ b/tests/fixture-projects/include/errors/badext/COND @@ -0,0 +1,6 @@ +include("myfile.py") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/defines-tasks/COND b/tests/fixture-projects/include/errors/defines-tasks/COND new file mode 100644 index 0000000..20ff586 --- /dev/null +++ b/tests/fixture-projects/include/errors/defines-tasks/COND @@ -0,0 +1,6 @@ +include("defs.cond") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/defines-tasks/defs.cond b/tests/fixture-projects/include/errors/defines-tasks/defs.cond new file mode 100644 index 0000000..34361df --- /dev/null +++ b/tests/fixture-projects/include/errors/defines-tasks/defs.cond @@ -0,0 +1,10 @@ +# Cannot define tasks in an included file. +run_command( + name="testing", + run="exit 0", +) + +run_experiment( + name="testing2", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/nested/COND b/tests/fixture-projects/include/errors/nested/COND new file mode 100644 index 0000000..21ae1f7 --- /dev/null +++ b/tests/fixture-projects/include/errors/nested/COND @@ -0,0 +1,6 @@ +include("one.cond") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/nested/one.cond b/tests/fixture-projects/include/errors/nested/one.cond new file mode 100644 index 0000000..418a827 --- /dev/null +++ b/tests/fixture-projects/include/errors/nested/one.cond @@ -0,0 +1,2 @@ +# Cannot call include again inside an included file. +include("two.cond") diff --git a/tests/fixture-projects/include/errors/nested/two.cond b/tests/fixture-projects/include/errors/nested/two.cond new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixture-projects/include/errors/nonexistent/COND b/tests/fixture-projects/include/errors/nonexistent/COND new file mode 100644 index 0000000..e15a721 --- /dev/null +++ b/tests/fixture-projects/include/errors/nonexistent/COND @@ -0,0 +1,6 @@ +include("nosuchfile.cond") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/nonexistent2/COND b/tests/fixture-projects/include/errors/nonexistent2/COND new file mode 100644 index 0000000..90ecc35 --- /dev/null +++ b/tests/fixture-projects/include/errors/nonexistent2/COND @@ -0,0 +1,6 @@ +include("//errors/nonexistent2/nosuchfile.cond") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/errors/outside-project/COND b/tests/fixture-projects/include/errors/outside-project/COND new file mode 100644 index 0000000..31b012b --- /dev/null +++ b/tests/fixture-projects/include/errors/outside-project/COND @@ -0,0 +1,6 @@ +include("../../experiments/include_test.cond") + +run_command( + name="main", + run="exit 0", +) diff --git a/tests/fixture-projects/include/sharing/COND b/tests/fixture-projects/include/sharing/COND new file mode 100644 index 0000000..d66dcf5 --- /dev/null +++ b/tests/fixture-projects/include/sharing/COND @@ -0,0 +1,7 @@ +combine( + name="both", + deps=[ + "//sharing/exp1:exp1", + "//sharing/exp2:exp2", + ], +) diff --git a/tests/fixture-projects/include/sharing/common.cond b/tests/fixture-projects/include/sharing/common.cond new file mode 100644 index 0000000..7f6ff7c --- /dev/null +++ b/tests/fixture-projects/include/sharing/common.cond @@ -0,0 +1 @@ +VALUE1 = 123 diff --git a/tests/fixture-projects/include/sharing/common2.cond b/tests/fixture-projects/include/sharing/common2.cond new file mode 100644 index 0000000..493f6d4 --- /dev/null +++ b/tests/fixture-projects/include/sharing/common2.cond @@ -0,0 +1 @@ +VALUE2 = 1337 diff --git a/tests/fixture-projects/include/sharing/echo.sh b/tests/fixture-projects/include/sharing/echo.sh new file mode 100755 index 0000000..4f20a7d --- /dev/null +++ b/tests/fixture-projects/include/sharing/echo.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +echo $1-$2 > $COND_OUT/out.txt diff --git a/tests/fixture-projects/include/sharing/exp1/COND b/tests/fixture-projects/include/sharing/exp1/COND new file mode 100644 index 0000000..673e756 --- /dev/null +++ b/tests/fixture-projects/include/sharing/exp1/COND @@ -0,0 +1,8 @@ +include("../common.cond") +include("../common2.cond") + +run_experiment( + name="exp1", + run="../echo.sh", + args=["exp1", (VALUE1 + VALUE2)], +) diff --git a/tests/fixture-projects/include/sharing/exp2/COND b/tests/fixture-projects/include/sharing/exp2/COND new file mode 100644 index 0000000..6b91335 --- /dev/null +++ b/tests/fixture-projects/include/sharing/exp2/COND @@ -0,0 +1,8 @@ +include("../common.cond") +include("../common2.cond") + +run_experiment( + name="exp2", + run="../echo.sh", + args=["exp2", (VALUE1 + VALUE2)], +) diff --git a/website/docs/directives.md b/website/docs/directives.md new file mode 100644 index 0000000..b0b9aa0 --- /dev/null +++ b/website/docs/directives.md @@ -0,0 +1,7 @@ +--- +title: Directives +id: directives +--- + +Directives are special "functions" that can be used in `COND` files. The +subpages in this section describe Conductor's directives in detail. diff --git a/website/docs/directives/include.md b/website/docs/directives/include.md new file mode 100644 index 0000000..e28d4c9 --- /dev/null +++ b/website/docs/directives/include.md @@ -0,0 +1,84 @@ +--- +title: include() +id: include +--- + +```python +include(path) +``` + +The `include()` directive allows you to "include" a separate file in a `COND` +file. The primary use case for `include()` is when you want to share common +configuration values among multiple `COND` files (for use across multiple task +definitions). You can define the configuration values in a single file, and then +`include()` that file in every relevant `COND` file. + +When encountering an `include()` directive, Conductor will interpret the +included file as a Python program. Any symbols (e.g., variables, functions) +defined in the included file will be usable inside the `COND` file. A `COND` +file can `include()` as many other files as needed. Note that `include()` +directives are processed in the order they are written in the `COND` file (i.e., +from top to bottom). + +Included files are meant to be used to share configuration values. As a result, +included files cannot `include()` other files (i.e., nested `include()`s are not +supported). An included file also cannot define any tasks. Included files must +also be *deterministic* (i.e., they must always produce the same results each +time they are evaluated). These restrictions are meant to keep `include()` +directives simple to reason about. + + +## Arguments + +### `path` + +**Type:** String (required) + +The path to the file to include, which will be interpreted as a Python program. +To distinguish Conductor includes from regular Python programs, all included +files must have a `.cond` extension. + +Paths can be specified either (i) relative to the `COND` file's location, or +(ii) relative to the project root. To specify a path relative to the project +root, use `//` to indicate the project root (see the usage example below). + + +## Usage Example + +In the example below, we define two `run_experiment()` tasks in separate `COND` +files. However, both tasks share configuration values that are defined in +`common.cond`. + +```python title=experiments/common.cond +# Common configuration values. +REPETITIONS = 3 +THREADS = 2 * 8 +``` + +```python title=experiments/baseline/COND {2} +# Include `common.cond` using a relative path. +include("../common.cond") + +run_experiment( + name="baseline", + run="./evaluate_baseline.sh", + options={ + "repetitions": REPETITIONS, + "threads": THREADS, + }, +) +``` + +```python title=experiments/new_system/COND {2} +# Include `common.cond` using a path relative to the project root. +include("//experiments/common.cond") + +run_experiment( + name="new_system", + run="./evaluate_new_system.sh", + options={ + "repetitions": REPETITIONS, + "threads": THREADS, + }, +) +``` diff --git a/website/sidebars.js b/website/sidebars.js index 6fccb72..dbe7076 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,6 +19,15 @@ module.exports = { 'task-types/run-experiment-group', ], }, + { + type: 'category', + label: 'Directives', + link: {type: 'doc', id: 'directives'}, + collapsed: false, + items: [ + 'directives/include', + ], + }, { type: 'category', label: 'Command Line Interface',