Skip to content

Commit baf65d6

Browse files
steveahnahnCopilot
authored andcommitted
Add Pre-commit check for airflowctl tests (apache#58856)
* pre-commit adds airflowctl int check * cleanup * remove asset materialize & others * dags update param, mege conflict, cicd error
1 parent 476e3f0 commit baf65d6

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

airflow-ctl-tests/tests/airflowctl_tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ def test_commands(login_command, date_param):
243243
login_command,
244244
# Assets commands
245245
"assets list",
246+
"assets get --asset-id=1",
247+
"assets create-event --asset-id=1",
246248
# Backfill commands
247249
"backfill list",
248250
# Config commands
@@ -263,12 +265,20 @@ def test_commands(login_command, date_param):
263265
# DAGs commands
264266
"dags list",
265267
"dags get --dag-id=example_bash_operator",
268+
"dags get-details --dag-id=example_bash_operator",
269+
"dags get-stats --dag-ids=example_bash_operator",
270+
"dags get-version --dag-id=example_bash_operator --version-number=1",
271+
"dags list-import-errors",
272+
"dags list-version --dag-id=example_bash_operator",
273+
"dags list-warning",
266274
# Order of trigger and pause/unpause is important for test stability because state checked
267275
f"dags trigger --dag-id=example_bash_operator --logical-date={date_param} --run-after={date_param}",
268276
"dags pause --dag-id=example_bash_operator",
269277
"dags unpause --dag-id=example_bash_operator",
270278
# DAG Run commands
271279
f'dagrun get --dag-id=example_bash_operator --dag-run-id="manual__{date_param}"',
280+
"dags update --dag-id=example_bash_operator --no-is-paused",
281+
# DAG Run commands
272282
"dagrun list --dag-id example_bash_operator --state success --limit=1",
273283
# Jobs commands
274284
"jobs list",

airflow-ctl/.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,13 @@ repos:
5353
^src/airflowctl/ctl/cli_config.py$|
5454
^src/airflowctl/api/operations.py$|
5555
^src/airflowctl/ctl/commands/.*\.py$
56+
- id: check-airflowctl-command-coverage
57+
name: Check airflowctl CLI command test coverage
58+
entry: ../scripts/ci/prek/check_airflowctl_command_coverage.py
59+
language: python
60+
pass_filenames: false
61+
files:
62+
(?x)
63+
^src/airflowctl/api/operations.py$|
64+
^../airflow-ctl-tests/tests/airflowctl_tests/conftest.py$|
65+
^../scripts/ci/prek/check_airflowctl_command_coverage.py$
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
# /// script
20+
# requires-python = ">=3.10,<3.11"
21+
# dependencies = [
22+
# "rich>=13.6.0",
23+
# ]
24+
# ///
25+
"""
26+
Check that all airflowctl CLI commands have integration test coverage by comparing commands from operations.py against test_commands in conftest.py.
27+
"""
28+
29+
from __future__ import annotations
30+
31+
import ast
32+
import re
33+
import sys
34+
from pathlib import Path
35+
36+
sys.path.insert(0, str(Path(__file__).parent.resolve()))
37+
from common_prek_utils import AIRFLOW_ROOT_PATH, console
38+
39+
OPERATIONS_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl" / "src" / "airflowctl" / "api" / "operations.py"
40+
CONFTEST_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl-tests" / "tests" / "airflowctl_tests" / "conftest.py"
41+
42+
# Operations excluded from CLI (see cli_config.py)
43+
EXCLUDED_OPERATION_CLASSES = {"BaseOperations", "LoginOperations", "VersionOperations"}
44+
EXCLUDED_METHODS = {
45+
"__init__",
46+
"__init_subclass__",
47+
"error",
48+
"_check_flag_and_exit_if_server_response_error",
49+
"bulk",
50+
}
51+
52+
EXCLUDED_COMMANDS = {
53+
"assets delete-dag-queued-events",
54+
"assets delete-queued-event",
55+
"assets delete-queued-events",
56+
"assets get-by-alias",
57+
"assets get-dag-queued-event",
58+
"assets get-dag-queued-events",
59+
"assets get-queued-events",
60+
"assets list-by-alias",
61+
"assets materialize",
62+
"backfill cancel",
63+
"backfill create",
64+
"backfill create-dry-run",
65+
"backfill get",
66+
"backfill pause",
67+
"backfill unpause",
68+
"connections create-defaults",
69+
"connections test",
70+
"dags delete",
71+
"dags get-import-error",
72+
"dags get-tags",
73+
}
74+
75+
76+
def parse_operations() -> dict[str, list[str]]:
77+
commands: dict[str, list[str]] = {}
78+
79+
with open(OPERATIONS_FILE) as f:
80+
tree = ast.parse(f.read(), filename=str(OPERATIONS_FILE))
81+
82+
for node in ast.walk(tree):
83+
if isinstance(node, ast.ClassDef) and node.name.endswith("Operations"):
84+
if node.name in EXCLUDED_OPERATION_CLASSES:
85+
continue
86+
87+
group_name = node.name.replace("Operations", "").lower()
88+
commands[group_name] = []
89+
90+
for child in node.body:
91+
if isinstance(child, ast.FunctionDef):
92+
method_name = child.name
93+
if method_name in EXCLUDED_METHODS or method_name.startswith("_"):
94+
continue
95+
subcommand = method_name.replace("_", "-")
96+
commands[group_name].append(subcommand)
97+
98+
return commands
99+
100+
101+
def parse_tested_commands() -> set[str]:
102+
tested: set[str] = set()
103+
104+
with open(CONFTEST_FILE) as f:
105+
content = f.read()
106+
107+
# Match command patterns like "assets list", "dags list-import-errors", etc.
108+
# Also handles f-strings like f"dagrun get..." or f'dagrun get...'
109+
pattern = r'f?["\']([a-z]+(?:-[a-z]+)*\s+[a-z]+(?:-[a-z]+)*)'
110+
for match in re.findall(pattern, content):
111+
parts = match.split()
112+
if len(parts) >= 2:
113+
tested.add(f"{parts[0]} {parts[1]}")
114+
115+
return tested
116+
117+
118+
def main():
119+
available = parse_operations()
120+
tested = parse_tested_commands()
121+
122+
missing = []
123+
for group, subcommands in sorted(available.items()):
124+
for subcommand in sorted(subcommands):
125+
cmd = f"{group} {subcommand}"
126+
if cmd not in tested and cmd not in EXCLUDED_COMMANDS:
127+
missing.append(cmd)
128+
129+
if missing:
130+
console.print("[red]ERROR: Commands not covered by integration tests:[/]")
131+
for cmd in missing:
132+
console.print(f" [red]- {cmd}[/]")
133+
console.print()
134+
console.print("[yellow]Fix by either:[/]")
135+
console.print("1. Add test to airflow-ctl-tests/tests/airflowctl_tests/conftest.py")
136+
console.print("2. Add to EXCLUDED_COMMANDS in scripts/ci/prek/check_airflowctl_command_coverage.py")
137+
sys.exit(1)
138+
139+
total = sum(len(cmds) for cmds in available.values())
140+
console.print(
141+
f"[green]All {total} CLI commands covered ({len(tested)} tested, {len(EXCLUDED_COMMANDS)} excluded)[/]"
142+
)
143+
sys.exit(0)
144+
145+
146+
if __name__ == "__main__":
147+
main()

0 commit comments

Comments
 (0)