tasktree 0.0.12__tar.gz → 0.0.14__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.14/.github/workflows/validate-pipx-install.yml +122 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/PKG-INFO +1 -1
- tasktree-0.0.14/example/tasktree.yaml +37 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/pyproject.toml +1 -1
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/cli.py +45 -3
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/substitution.py +39 -19
- tasktree-0.0.14/tests/e2e/test_non_docker.py +132 -0
- tasktree-0.0.14/tests/unit/test_cli.py +202 -0
- tasktree-0.0.12/example/tasktree.yaml +0 -16
- tasktree-0.0.12/tests/unit/test_cli.py +0 -109
- {tasktree-0.0.12 → tasktree-0.0.14}/.claude/settings.local.json +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.github/workflows/claude-code-review.yml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.github/workflows/claude.yml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.github/workflows/release.yml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.github/workflows/test.yml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.gitignore +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/.python-version +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/CLAUDE.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/README.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/example/source.txt +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/01-basic-variables.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/02-env-variable-type.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/03-direct-env-substitution.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/04-file-read-variables.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/docker-task-environments.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/requirements/implemented/shell-environment-requirements.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/schema/README.md +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/schema/tasktree-schema.json +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/schema/vscode-settings-snippet.json +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/__init__.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/__init__.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/docker.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/executor.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/graph.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/hasher.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/parser.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/state.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/tasks.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/src/tasktree/types.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tasktree.yaml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/e2e/__init__.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/e2e/test_docker_basic.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/e2e/test_docker_environment.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/e2e/test_docker_ownership.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/e2e/test_docker_volumes.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_arg_choices.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_arg_min_max.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_builtin_variables.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_clean_state.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_cli_options.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_dependency_execution.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_docker_build_args.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_end_to_end.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_exported_args.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_input_detection.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_missing_outputs.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_nested_imports.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_parameterized_dependencies.yaml +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_parameterized_deps_execution.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_state_persistence.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_variables.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/integration/test_working_directory.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_dependency_parsing.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_docker.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_environment_tracking.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_executor.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_graph.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_hasher.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_list_formatting.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_parameterized_graph.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_parser.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_state.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_substitution.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_tasks.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/tests/unit/test_types.py +0 -0
- {tasktree-0.0.12 → tasktree-0.0.14}/uv.lock +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
name: Validate pipx Installation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["Release to PyPI"]
|
|
6
|
+
types:
|
|
7
|
+
- completed
|
|
8
|
+
workflow_dispatch: # Allow manual triggering
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
validate-installation:
|
|
12
|
+
# Only run if the release workflow succeeded
|
|
13
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
include:
|
|
19
|
+
- os: ubuntu-latest
|
|
20
|
+
platform: Ubuntu
|
|
21
|
+
- os: macos-latest
|
|
22
|
+
platform: macOS
|
|
23
|
+
- os: windows-latest
|
|
24
|
+
platform: Windows
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout repository
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- name: Wait for PyPI propagation
|
|
31
|
+
if: github.event_name == 'workflow_run'
|
|
32
|
+
run: |
|
|
33
|
+
echo "Waiting 90 seconds for PyPI to propagate the new package..."
|
|
34
|
+
sleep 90
|
|
35
|
+
shell: bash
|
|
36
|
+
|
|
37
|
+
- name: Set up Python
|
|
38
|
+
uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: '3.12'
|
|
41
|
+
|
|
42
|
+
- name: Install pipx
|
|
43
|
+
run: |
|
|
44
|
+
python -m pip install --user pipx
|
|
45
|
+
python -m pipx ensurepath
|
|
46
|
+
shell: bash
|
|
47
|
+
|
|
48
|
+
- name: Install tasktree via pipx
|
|
49
|
+
run: |
|
|
50
|
+
python -m pipx install tasktree
|
|
51
|
+
shell: bash
|
|
52
|
+
|
|
53
|
+
- name: Test tt --list command
|
|
54
|
+
working-directory: example
|
|
55
|
+
run: |
|
|
56
|
+
tt --list
|
|
57
|
+
shell: bash
|
|
58
|
+
|
|
59
|
+
- name: Run test task
|
|
60
|
+
working-directory: example
|
|
61
|
+
run: |
|
|
62
|
+
tt package
|
|
63
|
+
shell: bash
|
|
64
|
+
|
|
65
|
+
- name: Verify installation
|
|
66
|
+
run: |
|
|
67
|
+
echo "✓ tasktree successfully installed via pipx on ${{ matrix.platform }}"
|
|
68
|
+
|
|
69
|
+
validate-centos:
|
|
70
|
+
# Only run if the release workflow succeeded
|
|
71
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
container:
|
|
74
|
+
image: quay.io/centos/centos:stream9
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- name: Checkout repository
|
|
78
|
+
uses: actions/checkout@v4
|
|
79
|
+
|
|
80
|
+
- name: Wait for PyPI propagation
|
|
81
|
+
if: github.event_name == 'workflow_run'
|
|
82
|
+
run: |
|
|
83
|
+
echo "Waiting 90 seconds for PyPI to propagate the new package..."
|
|
84
|
+
sleep 90
|
|
85
|
+
|
|
86
|
+
- name: Install dependencies
|
|
87
|
+
run: |
|
|
88
|
+
dnf install -y python python-pip
|
|
89
|
+
shell: bash
|
|
90
|
+
|
|
91
|
+
- name: Install Python 3.12
|
|
92
|
+
run: dnf install -y python3.12
|
|
93
|
+
shell: bash
|
|
94
|
+
|
|
95
|
+
- name: Install pipx with Python 3.12
|
|
96
|
+
run: |
|
|
97
|
+
pip install pipx
|
|
98
|
+
pipx ensurepath
|
|
99
|
+
shell: bash
|
|
100
|
+
|
|
101
|
+
- name: Install tasktree via pipx with Python 3.12
|
|
102
|
+
run: |
|
|
103
|
+
pipx install --python python3.12 tasktree
|
|
104
|
+
shell: bash
|
|
105
|
+
|
|
106
|
+
- name: Test tt --list command
|
|
107
|
+
working-directory: example
|
|
108
|
+
run: |
|
|
109
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
110
|
+
tt --list
|
|
111
|
+
shell: bash
|
|
112
|
+
|
|
113
|
+
- name: Run test task
|
|
114
|
+
working-directory: example
|
|
115
|
+
run: |
|
|
116
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
117
|
+
tt package
|
|
118
|
+
shell: bash
|
|
119
|
+
|
|
120
|
+
- name: Verify installation
|
|
121
|
+
run: |
|
|
122
|
+
echo "✓ tasktree successfully installed via pipx on CentOS Stream 9"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
variables:
|
|
2
|
+
log_level: { env: LOG_LEVEL, default: "info" }
|
|
3
|
+
|
|
4
|
+
tasks:
|
|
5
|
+
build:
|
|
6
|
+
desc: Building outputs (imagine this is a call to Cargo, or gcc, or something)
|
|
7
|
+
args:
|
|
8
|
+
- build_type:
|
|
9
|
+
choices: [ "debug", "release", "{{ var.log_level }}" ]
|
|
10
|
+
default: "{{ var.log_level }}"
|
|
11
|
+
- target:
|
|
12
|
+
type: str
|
|
13
|
+
choices:
|
|
14
|
+
- "x86"
|
|
15
|
+
- "x86_64"
|
|
16
|
+
- "arm"
|
|
17
|
+
- "ppc"
|
|
18
|
+
default: "x86_64"
|
|
19
|
+
outputs: [build/**]
|
|
20
|
+
cmd: |
|
|
21
|
+
echo building for {{ arg.target }}...
|
|
22
|
+
cd build
|
|
23
|
+
echo "code and stuff" > output-{{ arg.build_type }}-{{ arg.target }}.txt
|
|
24
|
+
|
|
25
|
+
package:
|
|
26
|
+
desc: Create archive
|
|
27
|
+
deps:
|
|
28
|
+
- build: [ "debug", "x86" ]
|
|
29
|
+
- build: [ "release" ]
|
|
30
|
+
- build: { build_type: "release", target: "ppc" }
|
|
31
|
+
outputs: [archive.tar.gz]
|
|
32
|
+
cmd: echo "making archive.tar.gz..."
|
|
33
|
+
|
|
34
|
+
clean:
|
|
35
|
+
desc: Clean generated files
|
|
36
|
+
inputs: [ archive.tar.gz ]
|
|
37
|
+
cmd: rm -f archive.tar.gz
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any, List, Optional
|
|
5
7
|
|
|
@@ -26,6 +28,46 @@ app = typer.Typer(
|
|
|
26
28
|
console = Console()
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def _supports_unicode() -> bool:
|
|
32
|
+
"""Check if the terminal supports Unicode characters.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if terminal supports UTF-8, False otherwise
|
|
36
|
+
"""
|
|
37
|
+
# Hard stop: classic Windows console (conhost)
|
|
38
|
+
if os.name == "nt" and "WT_SESSION" not in os.environ:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Encoding check
|
|
42
|
+
encoding = sys.stdout.encoding
|
|
43
|
+
if not encoding:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
"✓✗".encode(encoding)
|
|
48
|
+
return True
|
|
49
|
+
except UnicodeEncodeError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_action_success_string() -> str:
|
|
54
|
+
"""Get the appropriate success symbol based on terminal capabilities.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Unicode tick symbol (✓) if terminal supports UTF-8, otherwise "[ OK ]"
|
|
58
|
+
"""
|
|
59
|
+
return "✓" if _supports_unicode() else "[ OK ]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_action_failure_string() -> str:
|
|
63
|
+
"""Get the appropriate failure symbol based on terminal capabilities.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Unicode cross symbol (✗) if terminal supports UTF-8, otherwise "[ FAIL ]"
|
|
67
|
+
"""
|
|
68
|
+
return "✗" if _supports_unicode() else "[ FAIL ]"
|
|
69
|
+
|
|
70
|
+
|
|
29
71
|
def _format_task_arguments(arg_specs: list[str | dict]) -> str:
|
|
30
72
|
"""Format task arguments for display in list output.
|
|
31
73
|
|
|
@@ -324,7 +366,7 @@ def _clean_state(tasks_file: Optional[str] = None) -> None:
|
|
|
324
366
|
|
|
325
367
|
if state_path.exists():
|
|
326
368
|
state_path.unlink()
|
|
327
|
-
console.print(f"[green]
|
|
369
|
+
console.print(f"[green]{get_action_success_string()} Removed {state_path}[/green]")
|
|
328
370
|
console.print("All tasks will run fresh on next execution")
|
|
329
371
|
else:
|
|
330
372
|
console.print(f"[yellow]No state file found at {state_path}[/yellow]")
|
|
@@ -410,9 +452,9 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
410
452
|
state.save()
|
|
411
453
|
try:
|
|
412
454
|
executor.execute_task(task_name, args_dict, force=force, only=only)
|
|
413
|
-
console.print(f"[green]
|
|
455
|
+
console.print(f"[green]{get_action_success_string()} Task '{task_name}' completed successfully[/green]")
|
|
414
456
|
except Exception as e:
|
|
415
|
-
console.print(f"[red]
|
|
457
|
+
console.print(f"[red]{get_action_failure_string()} Task '{task_name}' failed: {e}[/red]")
|
|
416
458
|
raise typer.Exit(1)
|
|
417
459
|
|
|
418
460
|
|
|
@@ -5,6 +5,7 @@ and {{ env.NAME }} placeholders with their corresponding values.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
+
from random import choice
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
|
|
@@ -15,11 +16,11 @@ PLACEHOLDER_PATTERN = re.compile(
|
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def substitute_variables(text: str, variables: dict[str, str]) -> str:
|
|
19
|
+
def substitute_variables(text: str | dict[str, Any], variables: dict[str, str]) -> str | dict[str, Any]:
|
|
19
20
|
"""Substitute {{ var.name }} placeholders with variable values.
|
|
20
21
|
|
|
21
22
|
Args:
|
|
22
|
-
text: Text containing {{ var.name }} placeholders
|
|
23
|
+
text: Text containing {{ var.name }} placeholders, or an argument dict with elements to be substituted
|
|
23
24
|
variables: Dictionary mapping variable names to their string values
|
|
24
25
|
|
|
25
26
|
Returns:
|
|
@@ -28,23 +29,42 @@ def substitute_variables(text: str, variables: dict[str, str]) -> str:
|
|
|
28
29
|
Raises:
|
|
29
30
|
ValueError: If a referenced variable is not defined
|
|
30
31
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
if isinstance(text, dict):
|
|
33
|
+
# The dict will only contain a single key, the value of this key should also be a dictionary, which contains
|
|
34
|
+
# the actual details of the argument.
|
|
35
|
+
assert len(text.keys()) == 1
|
|
36
|
+
|
|
37
|
+
for arg_name in text.keys():
|
|
38
|
+
# Pull out and substitute the individual fields of an argument one at a time
|
|
39
|
+
for field in [ "default", "min", "max" ]:
|
|
40
|
+
if field in text[arg_name]:
|
|
41
|
+
text[arg_name][field] = substitute_variables(text[arg_name][field], variables)
|
|
42
|
+
|
|
43
|
+
# choices is a list of things
|
|
44
|
+
if "choices" in text[arg_name]:
|
|
45
|
+
text[arg_name]["choices"] = [substitute_variables(c, variables) for c in text[arg_name]["choices"]]
|
|
46
|
+
|
|
47
|
+
return text
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("Empty arg dictionary")
|
|
50
|
+
else:
|
|
51
|
+
def replace_match(match: re.Match) -> str:
|
|
52
|
+
prefix = match.group(1)
|
|
53
|
+
name = match.group(2)
|
|
54
|
+
|
|
55
|
+
# Only substitute var: placeholders
|
|
56
|
+
if prefix != "var":
|
|
57
|
+
return match.group(0) # Return unchanged
|
|
58
|
+
|
|
59
|
+
if name not in variables:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Variable '{name}' is not defined. "
|
|
62
|
+
f"Variables must be defined before use."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return variables[name]
|
|
66
|
+
|
|
67
|
+
return PLACEHOLDER_PATTERN.sub(replace_match, text)
|
|
48
68
|
|
|
49
69
|
|
|
50
70
|
def substitute_arguments(text: str, args: dict[str, Any], exported_args: set[str] | None = None) -> str:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""E2E tests for non-docker environments."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
|
|
7
|
+
from . import run_tasktree_cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestNonDockerEnvironment(unittest.TestCase):
|
|
11
|
+
"""Test basic task execution without Docker."""
|
|
12
|
+
|
|
13
|
+
def test_simple_parameterized_task(self):
|
|
14
|
+
"""Test that parameterized task executes with default environment value."""
|
|
15
|
+
with TemporaryDirectory() as tmpdir:
|
|
16
|
+
project_root = Path(tmpdir)
|
|
17
|
+
|
|
18
|
+
# Create recipe with parameterized task
|
|
19
|
+
(project_root / "tasktree.yaml").write_text("""
|
|
20
|
+
tasks:
|
|
21
|
+
deploy:
|
|
22
|
+
args:
|
|
23
|
+
- foo
|
|
24
|
+
- environment: { type: str, choices: ["dev", "staging", "prod"], default: "dev" }
|
|
25
|
+
outputs: [deploy.log]
|
|
26
|
+
cmd: |
|
|
27
|
+
echo "environment={{ arg.environment }}" > deploy.log
|
|
28
|
+
echo "foo was {{ arg.foo }}"
|
|
29
|
+
""")
|
|
30
|
+
|
|
31
|
+
# Execute with positional argument and default environment
|
|
32
|
+
result = run_tasktree_cli(["deploy", "42"], cwd=project_root)
|
|
33
|
+
|
|
34
|
+
# Assert success
|
|
35
|
+
self.assertEqual(
|
|
36
|
+
result.returncode,
|
|
37
|
+
0,
|
|
38
|
+
f"CLI failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Verify output file
|
|
42
|
+
output_file = project_root / "deploy.log"
|
|
43
|
+
self.assertTrue(output_file.exists(), "Output file not created")
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
output_file.read_text().strip(),
|
|
46
|
+
"environment=dev"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Verify terminal output
|
|
50
|
+
self.assertIn("foo was 42", result.stdout)
|
|
51
|
+
|
|
52
|
+
def test_parameterized_task_with_custom_environment(self):
|
|
53
|
+
"""Test that parameterized task executes with explicit environment value."""
|
|
54
|
+
with TemporaryDirectory() as tmpdir:
|
|
55
|
+
project_root = Path(tmpdir)
|
|
56
|
+
|
|
57
|
+
# Create recipe with parameterized task
|
|
58
|
+
(project_root / "tasktree.yaml").write_text("""
|
|
59
|
+
tasks:
|
|
60
|
+
deploy:
|
|
61
|
+
args:
|
|
62
|
+
- foo
|
|
63
|
+
- environment: { type: str, choices: ["dev", "staging", "prod"], default: "dev" }
|
|
64
|
+
outputs: [deploy.log]
|
|
65
|
+
cmd: |
|
|
66
|
+
echo "environment={{ arg.environment }}" > deploy.log
|
|
67
|
+
echo "foo was {{ arg.foo }}"
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
# Execute with both arguments
|
|
71
|
+
result = run_tasktree_cli(["deploy", "42", "environment=prod"], cwd=project_root)
|
|
72
|
+
|
|
73
|
+
# Assert success
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
result.returncode,
|
|
76
|
+
0,
|
|
77
|
+
f"CLI failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Verify output file
|
|
81
|
+
output_file = project_root / "deploy.log"
|
|
82
|
+
self.assertTrue(output_file.exists(), "Output file not created")
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
output_file.read_text().strip(),
|
|
85
|
+
"environment=prod"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Verify terminal output
|
|
89
|
+
self.assertIn("foo was 42", result.stdout)
|
|
90
|
+
|
|
91
|
+
def test_list_tasks_output(self):
|
|
92
|
+
"""Test that --list displays tasks with correct formatting."""
|
|
93
|
+
with TemporaryDirectory() as tmpdir:
|
|
94
|
+
project_root = Path(tmpdir)
|
|
95
|
+
|
|
96
|
+
# Create recipe with parameterized task
|
|
97
|
+
(project_root / "tasktree.yaml").write_text("""
|
|
98
|
+
tasks:
|
|
99
|
+
deploy:
|
|
100
|
+
desc: Deploy to environment
|
|
101
|
+
args:
|
|
102
|
+
- foo
|
|
103
|
+
- environment: { type: str, choices: ["dev", "staging", "prod"], default: "dev" }
|
|
104
|
+
outputs: [deploy.log]
|
|
105
|
+
cmd: |
|
|
106
|
+
echo "environment={{ arg.environment }}" > deploy.log
|
|
107
|
+
echo "foo was {{ arg.foo }}"
|
|
108
|
+
""")
|
|
109
|
+
|
|
110
|
+
# Execute --list
|
|
111
|
+
result = run_tasktree_cli(["--list"], cwd=project_root)
|
|
112
|
+
|
|
113
|
+
# Assert success
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
result.returncode,
|
|
116
|
+
0,
|
|
117
|
+
f"CLI failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Verify task name is in output
|
|
121
|
+
self.assertIn("deploy", result.stdout)
|
|
122
|
+
|
|
123
|
+
# Verify description is in output
|
|
124
|
+
self.assertIn("Deploy to environment", result.stdout)
|
|
125
|
+
|
|
126
|
+
# Verify arguments are shown (the actual format includes ANSI codes, so we check for the argument names)
|
|
127
|
+
self.assertIn("foo", result.stdout)
|
|
128
|
+
self.assertIn("environment", result.stdout)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Tests for CLI argument parsing."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from tasktree.cli import (
|
|
10
|
+
_parse_task_args,
|
|
11
|
+
_supports_unicode,
|
|
12
|
+
get_action_failure_string,
|
|
13
|
+
get_action_success_string,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestParseTaskArgs(unittest.TestCase):
|
|
18
|
+
"""Tests for _parse_task_args() function."""
|
|
19
|
+
|
|
20
|
+
def test_parse_task_args_positional(self):
|
|
21
|
+
"""Test parsing positional arguments."""
|
|
22
|
+
arg_specs = ["environment", "region"]
|
|
23
|
+
arg_values = ["production", "us-east-1"]
|
|
24
|
+
|
|
25
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
26
|
+
|
|
27
|
+
self.assertEqual(result, {"environment": "production", "region": "us-east-1"})
|
|
28
|
+
|
|
29
|
+
def test_parse_task_args_named(self):
|
|
30
|
+
"""Test parsing name=value arguments."""
|
|
31
|
+
arg_specs = ["environment", "region"]
|
|
32
|
+
arg_values = ["environment=production", "region=us-east-1"]
|
|
33
|
+
|
|
34
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
35
|
+
|
|
36
|
+
self.assertEqual(result, {"environment": "production", "region": "us-east-1"})
|
|
37
|
+
|
|
38
|
+
def test_parse_task_args_with_defaults(self):
|
|
39
|
+
"""Test default values applied."""
|
|
40
|
+
arg_specs = ["environment", {"region": {"default": "us-west-1"}}]
|
|
41
|
+
arg_values = ["production"] # Only provide first arg
|
|
42
|
+
|
|
43
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
44
|
+
|
|
45
|
+
self.assertEqual(result, {"environment": "production", "region": "us-west-1"})
|
|
46
|
+
|
|
47
|
+
def test_parse_task_args_type_conversion(self):
|
|
48
|
+
"""Test values converted to correct types."""
|
|
49
|
+
arg_specs = [{"port": {"type": "int"}}, {"debug": {"type": "bool"}}, {"timeout": {"type": "float"}}]
|
|
50
|
+
arg_values = ["8080", "true", "30.5"]
|
|
51
|
+
|
|
52
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(result, {"port": 8080, "debug": True, "timeout": 30.5})
|
|
55
|
+
self.assertIsInstance(result["port"], int)
|
|
56
|
+
self.assertIsInstance(result["debug"], bool)
|
|
57
|
+
self.assertIsInstance(result["timeout"], float)
|
|
58
|
+
|
|
59
|
+
def test_parse_task_args_unknown_argument(self):
|
|
60
|
+
"""Test error for unknown argument name."""
|
|
61
|
+
arg_specs = ["environment"]
|
|
62
|
+
arg_values = ["unknown_arg=value"]
|
|
63
|
+
|
|
64
|
+
with self.assertRaises(typer.Exit):
|
|
65
|
+
_parse_task_args(arg_specs, arg_values)
|
|
66
|
+
|
|
67
|
+
def test_parse_task_args_too_many(self):
|
|
68
|
+
"""Test error for too many positional args."""
|
|
69
|
+
arg_specs = ["environment"]
|
|
70
|
+
arg_values = ["production", "extra_value"]
|
|
71
|
+
|
|
72
|
+
with self.assertRaises(typer.Exit):
|
|
73
|
+
_parse_task_args(arg_specs, arg_values)
|
|
74
|
+
|
|
75
|
+
def test_parse_task_args_missing_required(self):
|
|
76
|
+
"""Test error for missing required argument."""
|
|
77
|
+
arg_specs = ["environment", "region"]
|
|
78
|
+
arg_values = ["production"] # Missing 'region'
|
|
79
|
+
|
|
80
|
+
with self.assertRaises(typer.Exit):
|
|
81
|
+
_parse_task_args(arg_specs, arg_values)
|
|
82
|
+
|
|
83
|
+
def test_parse_task_args_invalid_type(self):
|
|
84
|
+
"""Test error for invalid type conversion."""
|
|
85
|
+
arg_specs = [{"port": {"type": "int"}}]
|
|
86
|
+
arg_values = ["not_a_number"]
|
|
87
|
+
|
|
88
|
+
with self.assertRaises(typer.Exit):
|
|
89
|
+
_parse_task_args(arg_specs, arg_values)
|
|
90
|
+
|
|
91
|
+
def test_parse_task_args_empty(self):
|
|
92
|
+
"""Test returns empty dict when no args."""
|
|
93
|
+
arg_specs = []
|
|
94
|
+
arg_values = []
|
|
95
|
+
|
|
96
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
97
|
+
|
|
98
|
+
self.assertEqual(result, {})
|
|
99
|
+
|
|
100
|
+
def test_parse_task_args_mixed(self):
|
|
101
|
+
"""Test mixing positional and named arguments."""
|
|
102
|
+
arg_specs = ["environment", "region", {"verbose": {"type": "bool"}}]
|
|
103
|
+
arg_values = ["production", "region=us-east-1", "verbose=true"]
|
|
104
|
+
|
|
105
|
+
result = _parse_task_args(arg_specs, arg_values)
|
|
106
|
+
|
|
107
|
+
self.assertEqual(result, {
|
|
108
|
+
"environment": "production",
|
|
109
|
+
"region": "us-east-1",
|
|
110
|
+
"verbose": True
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestUnicodeSupport(unittest.TestCase):
|
|
115
|
+
"""Tests for Unicode symbol detection functions."""
|
|
116
|
+
|
|
117
|
+
@patch('tasktree.cli.os.environ', {})
|
|
118
|
+
@patch('tasktree.cli.os.name', 'posix')
|
|
119
|
+
@patch('tasktree.cli.sys.stdout')
|
|
120
|
+
def test_supports_unicode_with_utf8_encoding(self, mock_stdout):
|
|
121
|
+
"""Test that UTF-8 encoding returns True."""
|
|
122
|
+
mock_stdout.encoding = 'utf-8'
|
|
123
|
+
self.assertTrue(_supports_unicode())
|
|
124
|
+
|
|
125
|
+
@patch('tasktree.cli.os.environ', {})
|
|
126
|
+
@patch('tasktree.cli.os.name', 'posix')
|
|
127
|
+
@patch('tasktree.cli.sys.stdout')
|
|
128
|
+
def test_supports_unicode_with_utf8_uppercase(self, mock_stdout):
|
|
129
|
+
"""Test that UTF-8 (uppercase) encoding returns True."""
|
|
130
|
+
mock_stdout.encoding = 'UTF-8'
|
|
131
|
+
self.assertTrue(_supports_unicode())
|
|
132
|
+
|
|
133
|
+
@patch('tasktree.cli.os.environ', {})
|
|
134
|
+
@patch('tasktree.cli.os.name', 'nt')
|
|
135
|
+
@patch('tasktree.cli.sys.stdout')
|
|
136
|
+
def test_supports_unicode_on_classic_windows_console(self, mock_stdout):
|
|
137
|
+
"""Test that classic Windows console (conhost) returns False."""
|
|
138
|
+
mock_stdout.encoding = 'utf-8'
|
|
139
|
+
# No WT_SESSION in environ means classic console
|
|
140
|
+
self.assertFalse(_supports_unicode())
|
|
141
|
+
|
|
142
|
+
@patch('tasktree.cli.os.environ', {'WT_SESSION': 'some-value'})
|
|
143
|
+
@patch('tasktree.cli.os.name', 'nt')
|
|
144
|
+
@patch('tasktree.cli.sys.stdout')
|
|
145
|
+
def test_supports_unicode_on_windows_terminal(self, mock_stdout):
|
|
146
|
+
"""Test that Windows Terminal with UTF-8 returns True."""
|
|
147
|
+
mock_stdout.encoding = 'utf-8'
|
|
148
|
+
# WT_SESSION present means Windows Terminal
|
|
149
|
+
self.assertTrue(_supports_unicode())
|
|
150
|
+
|
|
151
|
+
@patch('tasktree.cli.os.environ', {})
|
|
152
|
+
@patch('tasktree.cli.os.name', 'posix')
|
|
153
|
+
@patch('tasktree.cli.sys.stdout')
|
|
154
|
+
def test_supports_unicode_with_encoding_that_fails_encode(self, mock_stdout):
|
|
155
|
+
"""Test that encoding that can't encode symbols returns False."""
|
|
156
|
+
# ASCII encoding will fail to encode ✓✗
|
|
157
|
+
mock_stdout.encoding = 'ascii'
|
|
158
|
+
self.assertFalse(_supports_unicode())
|
|
159
|
+
|
|
160
|
+
@patch('tasktree.cli.os.environ', {})
|
|
161
|
+
@patch('tasktree.cli.os.name', 'posix')
|
|
162
|
+
@patch('tasktree.cli.sys.stdout')
|
|
163
|
+
def test_supports_unicode_with_none_encoding(self, mock_stdout):
|
|
164
|
+
"""Test that None encoding returns False."""
|
|
165
|
+
mock_stdout.encoding = None
|
|
166
|
+
self.assertFalse(_supports_unicode())
|
|
167
|
+
|
|
168
|
+
@patch('tasktree.cli.os.environ', {})
|
|
169
|
+
@patch('tasktree.cli.os.name', 'posix')
|
|
170
|
+
@patch('tasktree.cli.sys.stdout')
|
|
171
|
+
def test_supports_unicode_with_latin1_encoding(self, mock_stdout):
|
|
172
|
+
"""Test that Latin-1 encoding returns False (can't encode symbols)."""
|
|
173
|
+
mock_stdout.encoding = 'latin-1'
|
|
174
|
+
self.assertFalse(_supports_unicode())
|
|
175
|
+
|
|
176
|
+
@patch('tasktree.cli._supports_unicode')
|
|
177
|
+
def test_get_action_success_string_with_unicode(self, mock_supports):
|
|
178
|
+
"""Test success string returns Unicode symbol when supported."""
|
|
179
|
+
mock_supports.return_value = True
|
|
180
|
+
self.assertEqual(get_action_success_string(), "✓")
|
|
181
|
+
|
|
182
|
+
@patch('tasktree.cli._supports_unicode')
|
|
183
|
+
def test_get_action_success_string_without_unicode(self, mock_supports):
|
|
184
|
+
"""Test success string returns ASCII when Unicode not supported."""
|
|
185
|
+
mock_supports.return_value = False
|
|
186
|
+
self.assertEqual(get_action_success_string(), "[ OK ]")
|
|
187
|
+
|
|
188
|
+
@patch('tasktree.cli._supports_unicode')
|
|
189
|
+
def test_get_action_failure_string_with_unicode(self, mock_supports):
|
|
190
|
+
"""Test failure string returns Unicode symbol when supported."""
|
|
191
|
+
mock_supports.return_value = True
|
|
192
|
+
self.assertEqual(get_action_failure_string(), "✗")
|
|
193
|
+
|
|
194
|
+
@patch('tasktree.cli._supports_unicode')
|
|
195
|
+
def test_get_action_failure_string_without_unicode(self, mock_supports):
|
|
196
|
+
"""Test failure string returns ASCII when Unicode not supported."""
|
|
197
|
+
mock_supports.return_value = False
|
|
198
|
+
self.assertEqual(get_action_failure_string(), "[ FAIL ]")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
unittest.main()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
tasks:
|
|
2
|
-
process:
|
|
3
|
-
desc: Process source file
|
|
4
|
-
inputs: [source.txt]
|
|
5
|
-
outputs: [output.txt]
|
|
6
|
-
cmd: cat source.txt > output.txt
|
|
7
|
-
|
|
8
|
-
package:
|
|
9
|
-
desc: Create archive
|
|
10
|
-
deps: [process]
|
|
11
|
-
outputs: [archive.tar.gz]
|
|
12
|
-
cmd: tar czf archive.tar.gz output.txt
|
|
13
|
-
|
|
14
|
-
clean:
|
|
15
|
-
desc: Clean generated files
|
|
16
|
-
cmd: rm -f output.txt archive.tar.gz
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"""Tests for CLI argument parsing."""
|
|
2
|
-
|
|
3
|
-
import unittest
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
|
|
8
|
-
from tasktree.cli import _parse_task_args
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class TestParseTaskArgs(unittest.TestCase):
|
|
12
|
-
"""Tests for _parse_task_args() function."""
|
|
13
|
-
|
|
14
|
-
def test_parse_task_args_positional(self):
|
|
15
|
-
"""Test parsing positional arguments."""
|
|
16
|
-
arg_specs = ["environment", "region"]
|
|
17
|
-
arg_values = ["production", "us-east-1"]
|
|
18
|
-
|
|
19
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
20
|
-
|
|
21
|
-
self.assertEqual(result, {"environment": "production", "region": "us-east-1"})
|
|
22
|
-
|
|
23
|
-
def test_parse_task_args_named(self):
|
|
24
|
-
"""Test parsing name=value arguments."""
|
|
25
|
-
arg_specs = ["environment", "region"]
|
|
26
|
-
arg_values = ["environment=production", "region=us-east-1"]
|
|
27
|
-
|
|
28
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
29
|
-
|
|
30
|
-
self.assertEqual(result, {"environment": "production", "region": "us-east-1"})
|
|
31
|
-
|
|
32
|
-
def test_parse_task_args_with_defaults(self):
|
|
33
|
-
"""Test default values applied."""
|
|
34
|
-
arg_specs = ["environment", {"region": {"default": "us-west-1"}}]
|
|
35
|
-
arg_values = ["production"] # Only provide first arg
|
|
36
|
-
|
|
37
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
38
|
-
|
|
39
|
-
self.assertEqual(result, {"environment": "production", "region": "us-west-1"})
|
|
40
|
-
|
|
41
|
-
def test_parse_task_args_type_conversion(self):
|
|
42
|
-
"""Test values converted to correct types."""
|
|
43
|
-
arg_specs = [{"port": {"type": "int"}}, {"debug": {"type": "bool"}}, {"timeout": {"type": "float"}}]
|
|
44
|
-
arg_values = ["8080", "true", "30.5"]
|
|
45
|
-
|
|
46
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
47
|
-
|
|
48
|
-
self.assertEqual(result, {"port": 8080, "debug": True, "timeout": 30.5})
|
|
49
|
-
self.assertIsInstance(result["port"], int)
|
|
50
|
-
self.assertIsInstance(result["debug"], bool)
|
|
51
|
-
self.assertIsInstance(result["timeout"], float)
|
|
52
|
-
|
|
53
|
-
def test_parse_task_args_unknown_argument(self):
|
|
54
|
-
"""Test error for unknown argument name."""
|
|
55
|
-
arg_specs = ["environment"]
|
|
56
|
-
arg_values = ["unknown_arg=value"]
|
|
57
|
-
|
|
58
|
-
with self.assertRaises(typer.Exit):
|
|
59
|
-
_parse_task_args(arg_specs, arg_values)
|
|
60
|
-
|
|
61
|
-
def test_parse_task_args_too_many(self):
|
|
62
|
-
"""Test error for too many positional args."""
|
|
63
|
-
arg_specs = ["environment"]
|
|
64
|
-
arg_values = ["production", "extra_value"]
|
|
65
|
-
|
|
66
|
-
with self.assertRaises(typer.Exit):
|
|
67
|
-
_parse_task_args(arg_specs, arg_values)
|
|
68
|
-
|
|
69
|
-
def test_parse_task_args_missing_required(self):
|
|
70
|
-
"""Test error for missing required argument."""
|
|
71
|
-
arg_specs = ["environment", "region"]
|
|
72
|
-
arg_values = ["production"] # Missing 'region'
|
|
73
|
-
|
|
74
|
-
with self.assertRaises(typer.Exit):
|
|
75
|
-
_parse_task_args(arg_specs, arg_values)
|
|
76
|
-
|
|
77
|
-
def test_parse_task_args_invalid_type(self):
|
|
78
|
-
"""Test error for invalid type conversion."""
|
|
79
|
-
arg_specs = [{"port": {"type": "int"}}]
|
|
80
|
-
arg_values = ["not_a_number"]
|
|
81
|
-
|
|
82
|
-
with self.assertRaises(typer.Exit):
|
|
83
|
-
_parse_task_args(arg_specs, arg_values)
|
|
84
|
-
|
|
85
|
-
def test_parse_task_args_empty(self):
|
|
86
|
-
"""Test returns empty dict when no args."""
|
|
87
|
-
arg_specs = []
|
|
88
|
-
arg_values = []
|
|
89
|
-
|
|
90
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
91
|
-
|
|
92
|
-
self.assertEqual(result, {})
|
|
93
|
-
|
|
94
|
-
def test_parse_task_args_mixed(self):
|
|
95
|
-
"""Test mixing positional and named arguments."""
|
|
96
|
-
arg_specs = ["environment", "region", {"verbose": {"type": "bool"}}]
|
|
97
|
-
arg_values = ["production", "region=us-east-1", "verbose=true"]
|
|
98
|
-
|
|
99
|
-
result = _parse_task_args(arg_specs, arg_values)
|
|
100
|
-
|
|
101
|
-
self.assertEqual(result, {
|
|
102
|
-
"environment": "production",
|
|
103
|
-
"region": "us-east-1",
|
|
104
|
-
"verbose": True
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if __name__ == "__main__":
|
|
109
|
-
unittest.main()
|
|
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.12 → tasktree-0.0.14}/requirements/implemented/bug-report-dependency-triggering.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree-0.0.12 → tasktree-0.0.14}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|