This is an automated email from the ASF dual-hosted git repository.
Lee-W pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 61f028989c0 [AIP-94] airflowctl jobs: add check command (#68534)
61f028989c0 is described below
commit 61f028989c027a240a7a10c47a81f6cdce9cdd8a
Author: PoAn Yang <[email protected]>
AuthorDate: Fri Aug 7 15:54:32 2026 +0900
[AIP-94] airflowctl jobs: add check command (#68534)
Signed-off-by: PoAn Yang <[email protected]>
---
.../src/airflow/cli/commands/jobs_command.py | 2 +
.../unit/cli/commands/test_command_deprecations.py | 2 +
airflow-ctl/docs/images/command_hashes.txt | 2 +-
airflow-ctl/docs/images/output_jobs.svg | 66 ++++++------
airflow-ctl/src/airflowctl/ctl/cli_config.py | 49 +++++++++
.../src/airflowctl/ctl}/commands/jobs_command.py | 43 +++-----
.../airflow_ctl/ctl/commands/test_jobs_command.py | 115 +++++++++++++++++++++
7 files changed, 220 insertions(+), 59 deletions(-)
diff --git a/airflow-core/src/airflow/cli/commands/jobs_command.py
b/airflow-core/src/airflow/cli/commands/jobs_command.py
index 194d8720db2..57ab4a1df7b 100644
--- a/airflow-core/src/airflow/cli/commands/jobs_command.py
+++ b/airflow-core/src/airflow/cli/commands/jobs_command.py
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING
from sqlalchemy import select
+from airflow.cli.utils import deprecated_for_airflowctl
from airflow.jobs.job import Job, JobState
from airflow.utils.net import get_hostname
from airflow.utils.providers_configuration_loader import
providers_configuration_loaded
@@ -29,6 +30,7 @@ if TYPE_CHECKING:
from sqlalchemy.orm import Session
+@deprecated_for_airflowctl("airflowctl jobs check")
@providers_configuration_loaded
@provide_session
def check(args, *, session: Session = NEW_SESSION) -> None:
diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
index 667a2dc7ff5..27aa75e4938 100644
--- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
+++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
@@ -36,6 +36,7 @@ from airflow.cli.commands import (
config_command,
connection_command,
dag_command,
+ jobs_command,
pool_command,
provider_command,
task_command,
@@ -86,6 +87,7 @@ MIGRATED_CLI_COMMANDS = [
(task_command.task_states_for_dag_run, "airflowctl tasks
states-for-dag-run"),
(task_command.task_clear, "airflowctl tasks clear"),
(task_command.task_failed_deps, "airflowctl tasks failed-deps"),
+ (jobs_command.check, "airflowctl jobs check"),
]
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index 65c22154bc3..a6fd590fcf6 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -6,7 +6,7 @@ config:a3d936cb15fe3b547bf6c82cf93d923f
connections:942f9f88cb908c28bf5c19159fc5065b
dags:ea477b70a1c3bcf8ed603cdbd11e5ab6
dagrun:07035226eaaff0a557d3ad9aab4b41b5
-jobs:a5b644c5da8889443bb40ee10b599270
+jobs:d4af478f28dae48ee18d43b998edc345
pools:19efe105b9515ab1926ebcaf0e028d71
providers:34502fe09dc0b8b0a13e7e46efdffda6
taskinstances:bea84117114c2438eb7e7026f6bb7042
diff --git a/airflow-ctl/docs/images/output_jobs.svg
b/airflow-ctl/docs/images/output_jobs.svg
index 13b31d2caed..9ff6f8753c1 100644
--- a/airflow-ctl/docs/images/output_jobs.svg
+++ b/airflow-ctl/docs/images/output_jobs.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 294.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 318.4"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,78 +19,82 @@
font-weight: 700;
}
- .terminal-656532750-matrix {
+ .terminal-1867393987-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-656532750-title {
+ .terminal-1867393987-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-656532750-r1 { fill: #ff8700 }
-.terminal-656532750-r2 { fill: #c5c8c6 }
-.terminal-656532750-r3 { fill: #808080 }
-.terminal-656532750-r4 { fill: #68a0b3 }
+ .terminal-1867393987-r1 { fill: #ff8700 }
+.terminal-1867393987-r2 { fill: #c5c8c6 }
+.terminal-1867393987-r3 { fill: #808080 }
+.terminal-1867393987-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-656532750-clip-terminal">
- <rect x="0" y="0" width="914.0" height="243.0" />
+ <clipPath id="terminal-1867393987-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="267.4" />
</clipPath>
- <clipPath id="terminal-656532750-line-0">
+ <clipPath id="terminal-1867393987-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-1">
+<clipPath id="terminal-1867393987-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-2">
+<clipPath id="terminal-1867393987-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-3">
+<clipPath id="terminal-1867393987-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-4">
+<clipPath id="terminal-1867393987-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-5">
+<clipPath id="terminal-1867393987-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-6">
+<clipPath id="terminal-1867393987-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-7">
+<clipPath id="terminal-1867393987-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-656532750-line-8">
+<clipPath id="terminal-1867393987-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-1867393987-line-9">
+ <rect x="0" y="221.1" width="915" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="292" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="316.4" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-656532750-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-1867393987-clip-terminal)">
- <g class="terminal-656532750-matrix">
- <text class="terminal-656532750-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-656532750-line-0)">Usage:</text><text
class="terminal-656532750-r3" x="85.4" y="20" textLength="183"
clip-path="url(#terminal-656532750-line-0)">airflowctl jobs</text><text
class="terminal-656532750-r2" x="268.4" y="20" textLength="24.4"
clip-path="url(#terminal-656532750-line-0)"> [</text><text
class="terminal-656532750-r4" x="292.8" y="20" textLength="24.4"
clip-path="url(#terminal [...]
-</text><text class="terminal-656532750-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-656532750-line-1)">
-</text><text class="terminal-656532750-r2" x="0" y="68.8" textLength="280.6"
clip-path="url(#terminal-656532750-line-2)">Perform Jobs operations</text><text
class="terminal-656532750-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-656532750-line-2)">
-</text><text class="terminal-656532750-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-656532750-line-3)">
-</text><text class="terminal-656532750-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-656532750-line-4)">Positional Arguments:</text><text
class="terminal-656532750-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-656532750-line-4)">
-</text><text class="terminal-656532750-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-656532750-line-5)">COMMAND</text><text
class="terminal-656532750-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-656532750-line-5)">
-</text><text class="terminal-656532750-r4" x="48.8" y="166.4"
textLength="48.8" clip-path="url(#terminal-656532750-line-6)">list</text><text
class="terminal-656532750-r2" x="170.8" y="166.4" textLength="597.8"
clip-path="url(#terminal-656532750-line-6)">List scheduler, triggerer, and other Airflow jobs</text><text
class="terminal-656532750-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-656532750-line-6)">
-</text><text class="terminal-656532750-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-656532750-line-7)">
-</text><text class="terminal-656532750-r1" x="0" y="215.2" textLength="97.6"
clip-path="url(#terminal-656532750-line-8)">Options:</text><text
class="terminal-656532750-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-656532750-line-8)">
-</text><text class="terminal-656532750-r4" x="24.4" y="239.6"
textLength="24.4" clip-path="url(#terminal-656532750-line-9)">-h</text><text
class="terminal-656532750-r2" x="48.8" y="239.6" textLength="24.4"
clip-path="url(#terminal-656532750-line-9)">, </text><text
class="terminal-656532750-r4" x="73.2" y="239.6" textLength="73.2"
clip-path="url(#terminal-656532750-line-9)">--help</text><text
class="terminal-656532750-r2" x="170.8" y="239.6" textLength="378.2"
clip-path="url(#termina [...]
+ <g class="terminal-1867393987-matrix">
+ <text class="terminal-1867393987-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-1867393987-line-0)">Usage:</text><text
class="terminal-1867393987-r3" x="85.4" y="20" textLength="183"
clip-path="url(#terminal-1867393987-line-0)">airflowctl jobs</text><text
class="terminal-1867393987-r2" x="268.4" y="20" textLength="24.4"
clip-path="url(#terminal-1867393987-line-0)"> [</text><text
class="terminal-1867393987-r4" x="292.8" y="20" textLength="24.4"
clip-path="url(#t [...]
+</text><text class="terminal-1867393987-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-1867393987-line-1)">
+</text><text class="terminal-1867393987-r2" x="0" y="68.8" textLength="280.6"
clip-path="url(#terminal-1867393987-line-2)">Perform Jobs operations</text><text
class="terminal-1867393987-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-1867393987-line-2)">
+</text><text class="terminal-1867393987-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-1867393987-line-3)">
+</text><text class="terminal-1867393987-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-1867393987-line-4)">Positional Arguments:</text><text
class="terminal-1867393987-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-1867393987-line-4)">
+</text><text class="terminal-1867393987-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-1867393987-line-5)">COMMAND</text><text
class="terminal-1867393987-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-1867393987-line-5)">
+</text><text class="terminal-1867393987-r4" x="48.8" y="166.4" textLength="61"
clip-path="url(#terminal-1867393987-line-6)">check</text><text
class="terminal-1867393987-r2" x="170.8" y="166.4" textLength="390.4"
clip-path="url(#terminal-1867393987-line-6)">Check if job(s) are still alive.</text><text
class="terminal-1867393987-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-1867393987-line-6)">
+</text><text class="terminal-1867393987-r4" x="48.8" y="190.8"
textLength="48.8" clip-path="url(#terminal-1867393987-line-7)">list</text><text
class="terminal-1867393987-r2" x="170.8" y="190.8" textLength="597.8"
clip-path="url(#terminal-1867393987-line-7)">List scheduler, triggerer, and other Airflow jobs</text><text
class="terminal-1867393987-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-1867393987-line-7)">
+</text><text class="terminal-1867393987-r2" x="915" y="215.2"
textLength="12.2" clip-path="url(#terminal-1867393987-line-8)">
+</text><text class="terminal-1867393987-r1" x="0" y="239.6" textLength="97.6"
clip-path="url(#terminal-1867393987-line-9)">Options:</text><text
class="terminal-1867393987-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-1867393987-line-9)">
+</text><text class="terminal-1867393987-r4" x="24.4" y="264" textLength="24.4"
clip-path="url(#terminal-1867393987-line-10)">-h</text><text
class="terminal-1867393987-r2" x="48.8" y="264" textLength="24.4"
clip-path="url(#terminal-1867393987-line-10)">, </text><text
class="terminal-1867393987-r4" x="73.2" y="264" textLength="73.2"
clip-path="url(#terminal-1867393987-line-10)">--help</text><text
class="terminal-1867393987-r2" x="170.8" y="264" textLength="378.2"
clip-path="url(#termi [...]
</text>
</g>
</g>
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 787a53ce074..bbf5e59f8e9 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -354,6 +354,35 @@ ARG_ACTION_ON_EXISTING_KEY = Arg(
choices=("overwrite", "fail", "skip"),
)
+# Jobs command args
+ARG_JOB_TYPE_FILTER = Arg(
+ flags=("--job-type",),
+ choices=("SchedulerJob", "TriggererJob", "DagProcessorJob"),
+ help="The type of job(s) that will be checked.",
+)
+ARG_JOB_HOSTNAME_FILTER = Arg(
+ flags=("--hostname",),
+ type=str,
+ default=None,
+ help="The hostname of job(s) that will be checked.",
+)
+ARG_JOB_LOCAL_FILTER = Arg(
+ flags=("--local",),
+ action="store_true",
+ help="If passed, this command will only show jobs from the local host.",
+)
+ARG_JOB_LIMIT = Arg(
+ flags=("--limit",),
+ type=positive_int(allow_zero=True),
+ default=1,
+ help="The number of recent jobs that will be checked. To disable limit,
set 0.",
+)
+ARG_JOB_ALLOW_MULTIPLE = Arg(
+ flags=("--allow-multiple",),
+ action="store_true",
+ help="If passed, this command will be successful even if multiple matching
alive jobs are found.",
+)
+
# Config arguments
ARG_CONFIG_SECTION = Arg(
flags=("--section",),
@@ -1185,6 +1214,21 @@ VARIABLE_COMMANDS = (
),
)
+JOB_COMMANDS = (
+ ActionCommand(
+ name="check",
+ help="Check if job(s) are still alive.",
+ func=lazy_load_command("airflowctl.ctl.commands.jobs_command.check"),
+ args=(
+ ARG_JOB_TYPE_FILTER,
+ ARG_JOB_HOSTNAME_FILTER,
+ ARG_JOB_LOCAL_FILTER,
+ ARG_JOB_LIMIT,
+ ARG_JOB_ALLOW_MULTIPLE,
+ ),
+ ),
+)
+
core_commands: list[CLICommand] = [
GroupCommand(
name="auth",
@@ -1207,6 +1251,11 @@ core_commands: list[CLICommand] = [
help="Manage Airflow Dags",
subcommands=DAG_COMMANDS,
),
+ GroupCommand(
+ name="jobs",
+ help="Manage Airflow jobs",
+ subcommands=JOB_COMMANDS,
+ ),
GroupCommand(
name="pools",
help="Manage Airflow pools",
diff --git a/airflow-core/src/airflow/cli/commands/jobs_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/jobs_command.py
similarity index 51%
copy from airflow-core/src/airflow/cli/commands/jobs_command.py
copy to airflow-ctl/src/airflowctl/ctl/commands/jobs_command.py
index 194d8720db2..0eb9a47a62a 100644
--- a/airflow-core/src/airflow/cli/commands/jobs_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/jobs_command.py
@@ -16,39 +16,28 @@
# under the License.
from __future__ import annotations
-from typing import TYPE_CHECKING
+import socket
-from sqlalchemy import select
+import rich
-from airflow.jobs.job import Job, JobState
-from airflow.utils.net import get_hostname
-from airflow.utils.providers_configuration_loader import
providers_configuration_loaded
-from airflow.utils.session import NEW_SESSION, provide_session
+from airflowctl.api.client import NEW_API_CLIENT, ClientKind,
provide_api_client
-if TYPE_CHECKING:
- from sqlalchemy.orm import Session
-
-@providers_configuration_loaded
-@provide_session
-def check(args, *, session: Session = NEW_SESSION) -> None:
+@provide_api_client(kind=ClientKind.CLI)
+def check(args, api_client=NEW_API_CLIENT) -> None:
"""Check if job(s) are still alive."""
- if args.allow_multiple and args.limit <= 1:
- raise SystemExit("To use option --allow-multiple, you must set the
limit to a value greater than 1.")
+ if args.allow_multiple and args.limit == 1:
+ raise SystemExit(
+ "To use option --allow-multiple, you must set the limit to a value
greater than 1 "
+ "or 0 to disable it."
+ )
if args.hostname and args.local:
- raise SystemExit("You can't use --hostname and --local at the same
time")
+ raise SystemExit("You can't use --hostname and --local at the same
time.")
- query = select(Job).where(Job.state ==
JobState.RUNNING).order_by(Job.latest_heartbeat.desc())
- if args.job_type:
- query = query.where(Job.job_type == args.job_type)
- if args.hostname:
- query = query.where(Job.hostname == args.hostname)
- if args.local:
- query = query.where(Job.hostname == get_hostname())
+ hostname = socket.getfqdn() if args.local else args.hostname
+ alive_jobs = api_client.jobs.list(job_type=args.job_type,
hostname=hostname, is_alive=True).jobs
if args.limit > 0:
- query = query.limit(args.limit)
-
- alive_jobs: list[Job] = [job for job in session.scalars(query) if
job.is_alive()]
+ alive_jobs = alive_jobs[: args.limit]
count_alive_jobs = len(alive_jobs)
if count_alive_jobs == 0:
@@ -56,6 +45,6 @@ def check(args, *, session: Session = NEW_SESSION) -> None:
if count_alive_jobs > 1 and not args.allow_multiple:
raise SystemExit(f"Found {count_alive_jobs} alive jobs. Expected only
one.")
if count_alive_jobs == 1:
- print("Found one alive job.")
+ rich.print("Found one alive job.")
else:
- print(f"Found {count_alive_jobs} alive jobs.")
+ rich.print(f"Found {count_alive_jobs} alive jobs.")
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_jobs_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_jobs_command.py
new file mode 100644
index 00000000000..697faa682c4
--- /dev/null
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_jobs_command.py
@@ -0,0 +1,115 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from types import SimpleNamespace
+from unittest import mock
+
+import pytest
+
+from airflowctl.ctl import cli_parser
+from airflowctl.ctl.commands import jobs_command
+
+
+def _make_jobs(count: int) -> SimpleNamespace:
+ """Build a stand-in for the ``JobCollectionResponse`` of ``count``
already-alive jobs."""
+ return SimpleNamespace(jobs=[SimpleNamespace(id=i) for i in range(count)])
+
+
+class TestCliJobsCheck:
+ parser = cli_parser.get_parser()
+
+ @pytest.mark.parametrize(
+ ("argv", "alive_count", "expected_output"),
+ [
+ (["jobs", "check", "--job-type", "SchedulerJob"], 1, "Found one
alive job."),
+ (["jobs", "check", "--limit", "100", "--allow-multiple"], 3,
"Found 3 alive jobs."),
+ (["jobs", "check", "--limit", "0", "--allow-multiple"], 3, "Found
3 alive jobs."),
+ (["jobs", "check", "--limit", "1"], 5, "Found one alive job."),
+ ],
+ )
+ def test_reports_alive_jobs(self, capsys, argv, alive_count,
expected_output):
+ api_client = mock.MagicMock()
+ api_client.jobs.list.return_value = _make_jobs(alive_count)
+
+ jobs_command.check(self.parser.parse_args(argv), api_client=api_client)
+
+ assert expected_output in capsys.readouterr().out
+
+ @pytest.mark.parametrize(
+ ("argv", "local_hostname", "expected_kwargs"),
+ [
+ (
+ ["jobs", "check", "--job-type", "SchedulerJob"],
+ None,
+ {"job_type": "SchedulerJob", "hostname": None, "is_alive":
True},
+ ),
+ (
+ ["jobs", "check", "--hostname", "HOSTNAME"],
+ None,
+ {"job_type": None, "hostname": "HOSTNAME", "is_alive": True},
+ ),
+ (
+ ["jobs", "check", "--local"],
+ "local-host",
+ {"job_type": None, "hostname": "local-host", "is_alive": True},
+ ),
+ ],
+ )
+ def test_forwards_filters_to_jobs_list(self, argv, local_hostname,
expected_kwargs):
+ api_client = mock.MagicMock()
+ api_client.jobs.list.return_value = _make_jobs(1)
+
+ with mock.patch("airflowctl.ctl.commands.jobs_command.socket.getfqdn",
return_value=local_hostname):
+ jobs_command.check(self.parser.parse_args(argv),
api_client=api_client)
+
+ api_client.jobs.list.assert_called_once_with(**expected_kwargs)
+
+ @pytest.mark.parametrize(
+ ("argv", "alive_count", "expected_error"),
+ [
+ (["jobs", "check"], 0, r"No alive jobs found."),
+ (["jobs", "check", "--limit", "100"], 3, r"Found 3 alive jobs.
Expected only one."),
+ ],
+ )
+ def test_exits_when_alive_count_unexpected(self, argv, alive_count,
expected_error):
+ api_client = mock.MagicMock()
+ api_client.jobs.list.return_value = _make_jobs(alive_count)
+
+ with pytest.raises(SystemExit, match=expected_error):
+ jobs_command.check(self.parser.parse_args(argv),
api_client=api_client)
+
+ @pytest.mark.parametrize(
+ ("argv", "expected_error"),
+ [
+ (
+ ["jobs", "check", "--allow-multiple"],
+ r"To use option --allow-multiple, you must set the limit to a
value greater than 1 "
+ r"or 0 to disable it.",
+ ),
+ (
+ ["jobs", "check", "--hostname", "h", "--local"],
+ r"You can't use --hostname and --local at the same time",
+ ),
+ ],
+ )
+ def test_rejects_invalid_argument_combinations(self, argv, expected_error):
+ api_client = mock.MagicMock()
+
+ with pytest.raises(SystemExit, match=expected_error):
+ jobs_command.check(self.parser.parse_args(argv),
api_client=api_client)
+ api_client.jobs.list.assert_not_called()