tasktree 0.0.9__tar.gz → 0.0.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tasktree-0.0.9 → tasktree-0.0.10}/PKG-INFO +1 -1
- {tasktree-0.0.9 → tasktree-0.0.10}/pyproject.toml +1 -1
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/docker.py +17 -9
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/hasher.py +8 -1
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/parser.py +16 -3
- tasktree-0.0.10/tests/integration/test_docker_build_args.py +79 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_docker.py +159 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_parser.py +67 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.claude/settings.local.json +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.github/workflows/claude-code-review.yml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.github/workflows/claude.yml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.github/workflows/release.yml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.github/workflows/test.yml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.gitignore +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/.python-version +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/CLAUDE.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/README.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/example/source.txt +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/example/tasktree.yaml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/01-basic-variables.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/02-env-variable-type.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/03-direct-env-substitution.md +0 -0
- {tasktree-0.0.9/requirements → tasktree-0.0.10/requirements/implemented}/04-file-read-variables.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
- {tasktree-0.0.9/requirements/future → tasktree-0.0.10/requirements/implemented}/docker-task-environments.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/shell-environment-requirements.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/schema/README.md +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/schema/tasktree-schema.json +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/schema/vscode-settings-snippet.json +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/__init__.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/__init__.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/cli.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/executor.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/graph.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/state.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/substitution.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/tasks.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/src/tasktree/types.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tasktree.yaml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/e2e/__init__.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/e2e/test_docker_basic.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/e2e/test_docker_environment.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/e2e/test_docker_ownership.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/e2e/test_docker_volumes.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_arg_choices.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_arg_min_max.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_builtin_variables.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_clean_state.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_cli_options.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_dependency_execution.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_end_to_end.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_exported_args.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_input_detection.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_missing_outputs.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_nested_imports.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_parameterized_dependencies.yaml +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_parameterized_deps_execution.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_state_persistence.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_variables.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/integration/test_working_directory.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_cli.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_dependency_parsing.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_environment_tracking.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_executor.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_graph.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_hasher.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_list_formatting.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_parameterized_graph.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_state.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_substitution.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_tasks.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/tests/unit/test_types.py +0 -0
- {tasktree-0.0.9 → tasktree-0.0.10}/uv.lock +0 -0
|
@@ -87,16 +87,24 @@ class DockerManager:
|
|
|
87
87
|
|
|
88
88
|
# Build the image
|
|
89
89
|
try:
|
|
90
|
+
docker_build_cmd = [
|
|
91
|
+
"docker",
|
|
92
|
+
"build",
|
|
93
|
+
"-t",
|
|
94
|
+
image_tag,
|
|
95
|
+
"-f",
|
|
96
|
+
str(dockerfile_path),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Add build args if environment has them (docker environments use dict for args)
|
|
100
|
+
if isinstance(env.args, dict):
|
|
101
|
+
for arg_name, arg_value in env.args.items():
|
|
102
|
+
docker_build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
|
|
103
|
+
|
|
104
|
+
docker_build_cmd.append(str(context_path))
|
|
105
|
+
|
|
90
106
|
subprocess.run(
|
|
91
|
-
|
|
92
|
-
"docker",
|
|
93
|
-
"build",
|
|
94
|
-
"-t",
|
|
95
|
-
image_tag,
|
|
96
|
-
"-f",
|
|
97
|
-
str(dockerfile_path),
|
|
98
|
-
str(context_path),
|
|
99
|
-
],
|
|
107
|
+
docker_build_cmd,
|
|
100
108
|
check=True,
|
|
101
109
|
capture_output=False, # Show build output to user
|
|
102
110
|
)
|
|
@@ -104,9 +104,16 @@ def hash_environment_definition(env) -> str:
|
|
|
104
104
|
# Import inside function to avoid circular dependency
|
|
105
105
|
from tasktree.parser import Environment
|
|
106
106
|
|
|
107
|
+
# Handle args - can be list (shell args) or dict (docker build args)
|
|
108
|
+
args_value = env.args
|
|
109
|
+
if isinstance(env.args, dict):
|
|
110
|
+
args_value = dict(sorted(env.args.items())) # Sort dict for determinism
|
|
111
|
+
elif isinstance(env.args, list):
|
|
112
|
+
args_value = sorted(env.args) # Sort list for determinism
|
|
113
|
+
|
|
107
114
|
data = {
|
|
108
115
|
"shell": env.shell,
|
|
109
|
-
"args":
|
|
116
|
+
"args": args_value,
|
|
110
117
|
"preamble": env.preamble,
|
|
111
118
|
"dockerfile": env.dockerfile,
|
|
112
119
|
"context": env.context,
|
|
@@ -31,7 +31,7 @@ class Environment:
|
|
|
31
31
|
|
|
32
32
|
name: str
|
|
33
33
|
shell: str = "" # Path to shell (required for shell envs, optional for Docker)
|
|
34
|
-
args: list[str] = field(default_factory=list)
|
|
34
|
+
args: list[str] | dict[str, str] = field(default_factory=list) # Shell args (list) or Docker build args (dict)
|
|
35
35
|
preamble: str = ""
|
|
36
36
|
# Docker-specific fields (presence of dockerfile indicates Docker environment)
|
|
37
37
|
dockerfile: str = "" # Path to Dockerfile
|
|
@@ -44,7 +44,7 @@ class Environment:
|
|
|
44
44
|
run_as_root: bool = False # If True, skip user mapping (run as root in container)
|
|
45
45
|
|
|
46
46
|
def __post_init__(self):
|
|
47
|
-
"""Ensure args is
|
|
47
|
+
"""Ensure args is in the correct format."""
|
|
48
48
|
if isinstance(self.args, str):
|
|
49
49
|
self.args = [self.args]
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ class Task:
|
|
|
60
60
|
inputs: list[str] = field(default_factory=list)
|
|
61
61
|
outputs: list[str] = field(default_factory=list)
|
|
62
62
|
working_dir: str = ""
|
|
63
|
-
args: list[str] = field(default_factory=list)
|
|
63
|
+
args: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts (each dict has single key: arg name)
|
|
64
64
|
source_file: str = "" # Track which file defined this task
|
|
65
65
|
env: str = "" # Environment name to use for execution
|
|
66
66
|
|
|
@@ -75,6 +75,19 @@ class Task:
|
|
|
75
75
|
if isinstance(self.args, str):
|
|
76
76
|
self.args = [self.args]
|
|
77
77
|
|
|
78
|
+
# Validate args is not a dict (common YAML mistake)
|
|
79
|
+
if isinstance(self.args, dict):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Task '{self.name}' has invalid 'args' syntax.\n\n"
|
|
82
|
+
f"Found dictionary syntax (without dashes):\n"
|
|
83
|
+
f" args:\n"
|
|
84
|
+
f" {list(self.args.keys())[0] if self.args else 'key'}: ...\n\n"
|
|
85
|
+
f"Correct syntax uses list format (with dashes):\n"
|
|
86
|
+
f" args:\n"
|
|
87
|
+
f" - {list(self.args.keys())[0] if self.args else 'key'}: ...\n\n"
|
|
88
|
+
f"Arguments must be defined as a list, not a dictionary."
|
|
89
|
+
)
|
|
90
|
+
|
|
78
91
|
|
|
79
92
|
@dataclass
|
|
80
93
|
class DependencyInvocation:
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Integration tests for Docker build args functionality."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from tasktree.cli import app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestDockerBuildArgs(unittest.TestCase):
|
|
14
|
+
"""Test Docker build args are passed correctly to docker build."""
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
"""Set up test fixtures."""
|
|
18
|
+
self.runner = CliRunner()
|
|
19
|
+
self.env = {"NO_COLOR": "1"}
|
|
20
|
+
|
|
21
|
+
def test_build_args_passed_to_dockerfile(self):
|
|
22
|
+
"""Test that build args are passed to docker build and used in Dockerfile."""
|
|
23
|
+
with TemporaryDirectory() as tmpdir:
|
|
24
|
+
project_root = Path(tmpdir)
|
|
25
|
+
|
|
26
|
+
# Create a Dockerfile that uses ARG statements
|
|
27
|
+
dockerfile = project_root / "Dockerfile"
|
|
28
|
+
dockerfile.write_text("""FROM alpine:latest
|
|
29
|
+
|
|
30
|
+
ARG BUILD_VERSION
|
|
31
|
+
ARG BUILD_DATE
|
|
32
|
+
ARG PYTHON_VERSION=3.11
|
|
33
|
+
|
|
34
|
+
RUN echo "Build version: $BUILD_VERSION" > /build-info.txt && \\
|
|
35
|
+
echo "Build date: $BUILD_DATE" >> /build-info.txt && \\
|
|
36
|
+
echo "Python version: $PYTHON_VERSION" >> /build-info.txt
|
|
37
|
+
|
|
38
|
+
CMD ["cat", "/build-info.txt"]
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
# Create recipe with Docker environment and build args
|
|
42
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
43
|
+
recipe_file.write_text("""
|
|
44
|
+
environments:
|
|
45
|
+
default: builder
|
|
46
|
+
builder:
|
|
47
|
+
dockerfile: ./Dockerfile
|
|
48
|
+
context: .
|
|
49
|
+
args:
|
|
50
|
+
BUILD_VERSION: "1.2.3"
|
|
51
|
+
BUILD_DATE: "2024-01-01"
|
|
52
|
+
PYTHON_VERSION: "3.12"
|
|
53
|
+
|
|
54
|
+
tasks:
|
|
55
|
+
build:
|
|
56
|
+
env: builder
|
|
57
|
+
cmd: echo "Build args test"
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
original_cwd = os.getcwd()
|
|
61
|
+
try:
|
|
62
|
+
os.chdir(project_root)
|
|
63
|
+
|
|
64
|
+
# Run the task - this will build the Docker image with build args
|
|
65
|
+
# Note: This test will be skipped in CI if Docker is not available
|
|
66
|
+
result = self.runner.invoke(app, ["build"], env=self.env)
|
|
67
|
+
|
|
68
|
+
# If Docker is not available, skip the test
|
|
69
|
+
if "Docker is not available" in result.stdout or result.exit_code != 0:
|
|
70
|
+
self.skipTest("Docker not available in test environment")
|
|
71
|
+
|
|
72
|
+
self.assertEqual(result.exit_code, 0)
|
|
73
|
+
|
|
74
|
+
finally:
|
|
75
|
+
os.chdir(original_cwd)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
unittest.main()
|
|
@@ -142,6 +142,39 @@ class TestIsDockerEnvironment(unittest.TestCase):
|
|
|
142
142
|
)
|
|
143
143
|
self.assertFalse(is_docker_environment(env))
|
|
144
144
|
|
|
145
|
+
def test_shell_environment_with_list_args(self):
|
|
146
|
+
"""Test that shell environments still work with list args (backward compatibility)."""
|
|
147
|
+
# Shell environments should use list args for shell arguments
|
|
148
|
+
env = Environment(
|
|
149
|
+
name="bash",
|
|
150
|
+
shell="bash",
|
|
151
|
+
args=["-c", "-e"], # List of shell arguments
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Verify it's recognized as a shell environment (not Docker)
|
|
155
|
+
self.assertFalse(is_docker_environment(env))
|
|
156
|
+
|
|
157
|
+
# Verify args are stored as a list
|
|
158
|
+
self.assertIsInstance(env.args, list)
|
|
159
|
+
self.assertEqual(env.args, ["-c", "-e"])
|
|
160
|
+
|
|
161
|
+
def test_docker_environment_with_dict_args(self):
|
|
162
|
+
"""Test that Docker environments use dict args for build arguments."""
|
|
163
|
+
# Docker environments should use dict args for build arguments
|
|
164
|
+
env = Environment(
|
|
165
|
+
name="builder",
|
|
166
|
+
dockerfile="./Dockerfile",
|
|
167
|
+
context=".",
|
|
168
|
+
args={"BUILD_VERSION": "1.0.0", "BUILD_DATE": "2024-01-01"},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Verify it's recognized as a Docker environment
|
|
172
|
+
self.assertTrue(is_docker_environment(env))
|
|
173
|
+
|
|
174
|
+
# Verify args are stored as a dict
|
|
175
|
+
self.assertIsInstance(env.args, dict)
|
|
176
|
+
self.assertEqual(env.args, {"BUILD_VERSION": "1.0.0", "BUILD_DATE": "2024-01-01"})
|
|
177
|
+
|
|
145
178
|
|
|
146
179
|
class TestResolveContainerWorkingDir(unittest.TestCase):
|
|
147
180
|
"""Test container working directory resolution."""
|
|
@@ -246,6 +279,132 @@ class TestDockerManager(unittest.TestCase):
|
|
|
246
279
|
self.assertEqual(build_call_args[3], "tt-env-builder")
|
|
247
280
|
self.assertEqual(build_call_args[4], "-f")
|
|
248
281
|
|
|
282
|
+
@patch("tasktree.docker.subprocess.run")
|
|
283
|
+
def test_build_command_with_build_args(self, mock_run):
|
|
284
|
+
"""Test that docker build command includes --build-arg flags."""
|
|
285
|
+
env = Environment(
|
|
286
|
+
name="builder",
|
|
287
|
+
dockerfile="./Dockerfile",
|
|
288
|
+
context=".",
|
|
289
|
+
args={"FOO": "fooable", "bar": "you're barred!"},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Mock docker inspect returning image ID
|
|
293
|
+
def mock_run_side_effect(*args, **kwargs):
|
|
294
|
+
cmd = args[0]
|
|
295
|
+
if "inspect" in cmd:
|
|
296
|
+
result = Mock()
|
|
297
|
+
result.stdout = "sha256:abc123def456\n"
|
|
298
|
+
return result
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
mock_run.side_effect = mock_run_side_effect
|
|
302
|
+
|
|
303
|
+
self.manager.ensure_image_built(env)
|
|
304
|
+
|
|
305
|
+
# Check that docker build was called with build args (2nd call, after docker --version)
|
|
306
|
+
build_call_args = mock_run.call_args_list[1][0][0]
|
|
307
|
+
|
|
308
|
+
# Verify basic command structure
|
|
309
|
+
self.assertEqual(build_call_args[0], "docker")
|
|
310
|
+
self.assertEqual(build_call_args[1], "build")
|
|
311
|
+
self.assertEqual(build_call_args[2], "-t")
|
|
312
|
+
self.assertEqual(build_call_args[3], "tt-env-builder")
|
|
313
|
+
self.assertEqual(build_call_args[4], "-f")
|
|
314
|
+
|
|
315
|
+
# Verify build args are included
|
|
316
|
+
self.assertIn("--build-arg", build_call_args)
|
|
317
|
+
|
|
318
|
+
# Find all build arg pairs
|
|
319
|
+
build_args = {}
|
|
320
|
+
for i, arg in enumerate(build_call_args):
|
|
321
|
+
if arg == "--build-arg":
|
|
322
|
+
arg_pair = build_call_args[i + 1]
|
|
323
|
+
key, value = arg_pair.split("=", 1)
|
|
324
|
+
build_args[key] = value
|
|
325
|
+
|
|
326
|
+
# Verify expected build args
|
|
327
|
+
self.assertEqual(build_args["FOO"], "fooable")
|
|
328
|
+
self.assertEqual(build_args["bar"], "you're barred!")
|
|
329
|
+
|
|
330
|
+
@patch("tasktree.docker.subprocess.run")
|
|
331
|
+
def test_build_command_with_empty_build_args(self, mock_run):
|
|
332
|
+
"""Test that docker build command works with empty build args dict."""
|
|
333
|
+
env = Environment(
|
|
334
|
+
name="builder",
|
|
335
|
+
dockerfile="./Dockerfile",
|
|
336
|
+
context=".",
|
|
337
|
+
args={},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Mock docker inspect returning image ID
|
|
341
|
+
def mock_run_side_effect(*args, **kwargs):
|
|
342
|
+
cmd = args[0]
|
|
343
|
+
if "inspect" in cmd:
|
|
344
|
+
result = Mock()
|
|
345
|
+
result.stdout = "sha256:abc123def456\n"
|
|
346
|
+
return result
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
mock_run.side_effect = mock_run_side_effect
|
|
350
|
+
|
|
351
|
+
self.manager.ensure_image_built(env)
|
|
352
|
+
|
|
353
|
+
# Check that docker build was called (2nd call, after docker --version)
|
|
354
|
+
build_call_args = mock_run.call_args_list[1][0][0]
|
|
355
|
+
|
|
356
|
+
# Verify basic command structure
|
|
357
|
+
self.assertEqual(build_call_args[0], "docker")
|
|
358
|
+
self.assertEqual(build_call_args[1], "build")
|
|
359
|
+
|
|
360
|
+
# Verify NO build args are included
|
|
361
|
+
self.assertNotIn("--build-arg", build_call_args)
|
|
362
|
+
|
|
363
|
+
@patch("tasktree.docker.subprocess.run")
|
|
364
|
+
def test_build_command_with_special_characters_in_args(self, mock_run):
|
|
365
|
+
"""Test that build args with special characters are handled correctly."""
|
|
366
|
+
env = Environment(
|
|
367
|
+
name="builder",
|
|
368
|
+
dockerfile="./Dockerfile",
|
|
369
|
+
context=".",
|
|
370
|
+
args={
|
|
371
|
+
"API_KEY": "sk-1234_abcd-5678",
|
|
372
|
+
"MESSAGE": "Hello, World!",
|
|
373
|
+
"PATH_WITH_SPACES": "/path/to/my files",
|
|
374
|
+
"SPECIAL_CHARS": "test=value&foo=bar",
|
|
375
|
+
},
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Mock docker inspect returning image ID
|
|
379
|
+
def mock_run_side_effect(*args, **kwargs):
|
|
380
|
+
cmd = args[0]
|
|
381
|
+
if "inspect" in cmd:
|
|
382
|
+
result = Mock()
|
|
383
|
+
result.stdout = "sha256:abc123def456\n"
|
|
384
|
+
return result
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
mock_run.side_effect = mock_run_side_effect
|
|
388
|
+
|
|
389
|
+
self.manager.ensure_image_built(env)
|
|
390
|
+
|
|
391
|
+
# Check that docker build was called (2nd call, after docker --version)
|
|
392
|
+
build_call_args = mock_run.call_args_list[1][0][0]
|
|
393
|
+
|
|
394
|
+
# Find all build arg pairs
|
|
395
|
+
build_args = {}
|
|
396
|
+
for i, arg in enumerate(build_call_args):
|
|
397
|
+
if arg == "--build-arg":
|
|
398
|
+
arg_pair = build_call_args[i + 1]
|
|
399
|
+
key, value = arg_pair.split("=", 1)
|
|
400
|
+
build_args[key] = value
|
|
401
|
+
|
|
402
|
+
# Verify special characters are preserved
|
|
403
|
+
self.assertEqual(build_args["API_KEY"], "sk-1234_abcd-5678")
|
|
404
|
+
self.assertEqual(build_args["MESSAGE"], "Hello, World!")
|
|
405
|
+
self.assertEqual(build_args["PATH_WITH_SPACES"], "/path/to/my files")
|
|
406
|
+
self.assertEqual(build_args["SPECIAL_CHARS"], "test=value&foo=bar")
|
|
407
|
+
|
|
249
408
|
def test_resolve_volume_mount_relative(self):
|
|
250
409
|
"""Test relative volume path resolution."""
|
|
251
410
|
volume = "./src:/workspace/src"
|
|
@@ -1540,6 +1540,73 @@ tasks:
|
|
|
1540
1540
|
self.assertEqual(len(recipe.tasks), 0)
|
|
1541
1541
|
|
|
1542
1542
|
|
|
1543
|
+
class TestArgsValidation(unittest.TestCase):
|
|
1544
|
+
"""Tests for validating task args must be a list, not a dict."""
|
|
1545
|
+
|
|
1546
|
+
def test_args_dict_syntax_raises_error(self):
|
|
1547
|
+
"""Test that dictionary syntax for args raises a helpful error."""
|
|
1548
|
+
with TemporaryDirectory() as tmpdir:
|
|
1549
|
+
recipe_path = Path(tmpdir) / "tasktree.yaml"
|
|
1550
|
+
recipe_path.write_text("""
|
|
1551
|
+
tasks:
|
|
1552
|
+
foo:
|
|
1553
|
+
args:
|
|
1554
|
+
x: {}
|
|
1555
|
+
y: { type: int, default: 10 }
|
|
1556
|
+
cmd: echo x = {{ arg.x }}, y = {{ arg.y }}
|
|
1557
|
+
""")
|
|
1558
|
+
|
|
1559
|
+
with self.assertRaises(ValueError) as cm:
|
|
1560
|
+
parse_recipe(recipe_path)
|
|
1561
|
+
|
|
1562
|
+
error_msg = str(cm.exception)
|
|
1563
|
+
self.assertIn("invalid 'args' syntax", error_msg)
|
|
1564
|
+
self.assertIn("dictionary syntax", error_msg)
|
|
1565
|
+
self.assertIn("list format", error_msg)
|
|
1566
|
+
self.assertIn("with dashes", error_msg)
|
|
1567
|
+
# Should show the first key as an example
|
|
1568
|
+
self.assertIn("x", error_msg)
|
|
1569
|
+
|
|
1570
|
+
def test_args_list_syntax_is_valid(self):
|
|
1571
|
+
"""Test that list syntax for args works correctly."""
|
|
1572
|
+
with TemporaryDirectory() as tmpdir:
|
|
1573
|
+
recipe_path = Path(tmpdir) / "tasktree.yaml"
|
|
1574
|
+
recipe_path.write_text("""
|
|
1575
|
+
tasks:
|
|
1576
|
+
foo:
|
|
1577
|
+
args:
|
|
1578
|
+
- x
|
|
1579
|
+
- y: { type: int, default: 10 }
|
|
1580
|
+
cmd: echo x = {{ arg.x }}, y = {{ arg.y }}
|
|
1581
|
+
""")
|
|
1582
|
+
|
|
1583
|
+
# Should parse without error
|
|
1584
|
+
recipe = parse_recipe(recipe_path)
|
|
1585
|
+
|
|
1586
|
+
# Verify the task was parsed correctly
|
|
1587
|
+
self.assertIn("foo", recipe.tasks)
|
|
1588
|
+
task = recipe.tasks["foo"]
|
|
1589
|
+
self.assertEqual(len(task.args), 2)
|
|
1590
|
+
|
|
1591
|
+
def test_args_empty_dict_raises_error(self):
|
|
1592
|
+
"""Test that even an empty dict for args raises an error."""
|
|
1593
|
+
with TemporaryDirectory() as tmpdir:
|
|
1594
|
+
recipe_path = Path(tmpdir) / "tasktree.yaml"
|
|
1595
|
+
recipe_path.write_text("""
|
|
1596
|
+
tasks:
|
|
1597
|
+
foo:
|
|
1598
|
+
args: {}
|
|
1599
|
+
cmd: echo hello
|
|
1600
|
+
""")
|
|
1601
|
+
|
|
1602
|
+
with self.assertRaises(ValueError) as cm:
|
|
1603
|
+
parse_recipe(recipe_path)
|
|
1604
|
+
|
|
1605
|
+
error_msg = str(cm.exception)
|
|
1606
|
+
self.assertIn("invalid 'args' syntax", error_msg)
|
|
1607
|
+
self.assertIn("dictionary syntax", error_msg)
|
|
1608
|
+
|
|
1609
|
+
|
|
1543
1610
|
class TestVariablesParsing(unittest.TestCase):
|
|
1544
1611
|
"""Test parsing of variables section with environment variable support."""
|
|
1545
1612
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tasktree-0.0.9/requirements → tasktree-0.0.10/requirements/implemented}/04-file-read-variables.md
RENAMED
|
File without changes
|
{tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/bug-report-dependency-triggering.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree-0.0.9 → tasktree-0.0.10}/requirements/implemented/shell-environment-requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|