This is an automated email from the ASF dual-hosted git repository.

turbaszek pushed a commit to branch v1-10-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v1-10-test by this push:
     new 547d03d  Add airflow upgrade-check command (MVP) (#9467)
547d03d is described below

commit 547d03df2124e4345258d546e1c05707668119d6
Author: Kamil BreguĊ‚a <[email protected]>
AuthorDate: Thu Sep 10 16:38:49 2020 +0200

    Add airflow upgrade-check command (MVP) (#9467)
    
    Co-authored-by: Tomek Urbaszek <[email protected]>
    Co-authored-by: Kaxil Naik <[email protected]>
---
 airflow/bin/cli.py                                 |  31 +++++-
 airflow/upgrade/__init__.py                        |  16 +++
 airflow/upgrade/checker.py                         |  37 +++++++
 airflow/upgrade/formatters.py                      | 114 +++++++++++++++++++++
 airflow/upgrade/problem.py                         |  40 ++++++++
 airflow/upgrade/rules/__init__.py                  |  16 +++
 airflow/upgrade/rules/base_rule.py                 |  51 +++++++++
 airflow/upgrade/rules/conn_type_is_not_nullable.py |  42 ++++++++
 airflow/utils/cli.py                               |  23 +++++
 docs/conf.py                                       |   1 +
 tests/upgrade/__init__.py                          |  16 +++
 tests/upgrade/rules/__init__.py                    |  16 +++
 tests/upgrade/rules/test_base_rule.py              |  26 +++++
 .../rules/test_conn_type_is_not_nullable.py        |  40 ++++++++
 tests/upgrade/test_formattes.py                    |  54 ++++++++++
 tests/upgrade/test_problem.py                      |  35 +++++++
 16 files changed, 553 insertions(+), 5 deletions(-)

diff --git a/airflow/bin/cli.py b/airflow/bin/cli.py
index fbc170d..45f1c5a 100644
--- a/airflow/bin/cli.py
+++ b/airflow/bin/cli.py
@@ -72,6 +72,8 @@ from airflow.models import (
 )
 from airflow.ti_deps.dep_context import (DepContext, SCHEDULER_QUEUED_DEPS)
 from airflow.typing_compat import Protocol
+from airflow.upgrade.checker import check_upgrade
+from airflow.upgrade.formatters import (ConsoleFormatter, JSONFormatter)
 from airflow.utils import cli as cli_utils, db
 from airflow.utils.dot_renderer import render_dag
 from airflow.utils.net import get_hostname
@@ -2273,6 +2275,19 @@ def info(args):
         print(info)
 
 
+def upgrade_check(args):
+    if args.save:
+        filename = args.save
+        if not filename.lower().endswith(".json"):
+            print("Only JSON files are supported", file=sys.stderr)
+        formatter = JSONFormatter(args.save)
+    else:
+        formatter = ConsoleFormatter()
+    all_problems = check_upgrade(formatter)
+    if all_problems:
+        sys.exit(1)
+
+
 class Arg(object):
     def __init__(self, flags=None, help=None, action=None, default=None, 
nargs=None,
                  type=None, choices=None, metavar=None):
@@ -2442,16 +2457,16 @@ class CLIFactory(object):
         # show_dag
         'save': Arg(
             ("-s", "--save"),
-            "Saves the result to the indicated file.\n"
+            "Saves the result to the indicated file. The file format is 
determined by the file extension.\n"
             "\n"
-            "The file format is determined by the file extension. For more 
information about supported "
-            "format, see: https://www.graphviz.org/doc/info/output.html\n";
+            "To see more information about supported format for show_dags 
command, see: "
+            "https://www.graphviz.org/doc/info/output.html\n";
             "\n"
             "If you want to create a PNG file then you should execute the 
following command:\n"
-            "airflow dags show <DAG_ID> --save output.png\n"
+            "airflow show_dag <DAG_ID> --save output.png\n"
             "\n"
             "If you want to create a DOT file then you should execute the 
following command:\n"
-            "airflow dags show <DAG_ID> --save output.dot\n"
+            "airflow show_dag <DAG_ID> --save output.dot\n"
         ),
         'imgcat': Arg(
             ("--imgcat", ),
@@ -3015,6 +3030,12 @@ class CLIFactory(object):
             'func': info,
             'args': ('anonymize', 'file_io', ),
         },
+        {
+            'name': 'upgrade_check',
+            'help': 'Check if you can upgrade to the new version.',
+            'func': upgrade_check,
+            'args': ('save', ),
+        },
     )
     subparsers_dict = {sp['func'].__name__: sp for sp in subparsers}
     dag_subparsers = (
diff --git a/airflow/upgrade/__init__.py b/airflow/upgrade/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/airflow/upgrade/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/airflow/upgrade/checker.py b/airflow/upgrade/checker.py
new file mode 100644
index 0000000..e8c6837
--- /dev/null
+++ b/airflow/upgrade/checker.py
@@ -0,0 +1,37 @@
+# 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 absolute_import
+from typing import List
+
+from airflow.upgrade.formatters import BaseFormatter
+from airflow.upgrade.problem import RuleStatus
+from airflow.upgrade.rules.base_rule import BaseRule, RULES
+
+ALL_RULES = [cls() for cls in RULES]  # type: List[BaseRule]
+
+
+def check_upgrade(formatter):
+    # type: (BaseFormatter) -> List[RuleStatus]
+    formatter.start_checking(ALL_RULES)
+    all_rule_statuses = []  # List[RuleStatus]
+    for rule in ALL_RULES:
+        rule_status = RuleStatus.from_rule(rule)
+        all_rule_statuses.append(rule_status)
+        formatter.on_next_rule_status(rule_status)
+    formatter.end_checking(all_rule_statuses)
+    return all_rule_statuses
diff --git a/airflow/upgrade/formatters.py b/airflow/upgrade/formatters.py
new file mode 100644
index 0000000..be408dc
--- /dev/null
+++ b/airflow/upgrade/formatters.py
@@ -0,0 +1,114 @@
+# 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 abc import ABCMeta
+from typing import List
+import json
+
+from airflow.upgrade.problem import RuleStatus
+from airflow.upgrade.rules.base_rule import BaseRule
+from airflow.utils.cli import header, get_terminal_size
+
+
+class BaseFormatter(object):
+    __metaclass__ = ABCMeta
+
+    def start_checking(self, all_rules):
+        # type: (List[BaseRule]) -> None
+        pass
+
+    def end_checking(self, rule_statuses):
+        # type: (List[RuleStatus]) -> None
+
+        pass
+
+    def on_next_rule_status(self, rule_status):
+        # type: (RuleStatus) -> None
+        pass
+
+
+class ConsoleFormatter(BaseFormatter):
+    def start_checking(self, all_rules):
+        print()
+        header("STATUS", "=")
+        print()
+
+    def end_checking(self, rule_statuses):
+        if rule_statuses:
+            messages_count = sum(
+                len(rule_status.messages)
+                for rule_status in rule_statuses
+            )
+            if messages_count == 1:
+                print("Found {} problem.".format(messages_count))
+            else:
+                print("Found {} problems.".format(messages_count))
+            print()
+            header("RECOMMENDATIONS", "=")
+            print()
+
+            self.display_recommendations(rule_statuses)
+        else:
+            print("Not found any problems. World is beautiful. ")
+            print("You can safely update Airflow to the new version.")
+
+    @staticmethod
+    def display_recommendations(rule_statuses):
+
+        for rule_status in rule_statuses:
+            rule = rule_status.rule
+            print(rule.title)
+            print("-" * len(rule.title))
+            print(rule.description)
+            print("")
+            if rule_status.messages:
+                print("Problems:")
+                for message_no, message in enumerate(rule_status.messages, 1):
+                    print('{:>3}.  {}'.format(message_no, message))
+
+    def on_next_rule_status(self, rule_status):
+        status = "SUCCESS" if rule_status.is_success else "FAIL"
+        status_line_fmt = self.prepare_status_line_format()
+        print(status_line_fmt.format(rule_status.rule.title, status))
+
+    @staticmethod
+    def prepare_status_line_format():
+        _, terminal_width = get_terminal_size()
+
+        return "{:.<" + str(terminal_width - 10) + "}{:.>10}"
+
+
+class JSONFormatter(BaseFormatter):
+    def __init__(self, output_path):
+        self.filename = output_path
+
+    def start_checking(self, all_rules):
+        print("Start looking for problems.")
+
+    @staticmethod
+    def _info_from_rule_status(rule_status):
+        return {
+            "rule": type(rule_status.rule).__name__,
+            "title": rule_status.rule.title,
+            "messages": rule_status.messages,
+        }
+
+    def end_checking(self, rule_statuses):
+        formatted_results = [self._info_from_rule_status(rs) for rs in 
rule_statuses]
+        with open(self.filename, "w+") as output_file:
+            json.dump(formatted_results, output_file, indent=2)
+        print("Saved result to: {}".format(self.filename))
diff --git a/airflow/upgrade/problem.py b/airflow/upgrade/problem.py
new file mode 100644
index 0000000..d5960e6
--- /dev/null
+++ b/airflow/upgrade/problem.py
@@ -0,0 +1,40 @@
+# 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 absolute_import
+from typing import NamedTuple, List
+
+from airflow.upgrade.rules.base_rule import BaseRule
+
+
+class RuleStatus(NamedTuple(
+    'RuleStatus',
+    [
+        ('rule', BaseRule),
+        ('messages', List[str])
+    ]
+)):
+
+    @property
+    def is_success(self):
+        return bool(self.messages)
+
+    @classmethod
+    def from_rule(cls, rule):
+        # type: (BaseRule) -> RuleStatus
+        messages = rule.check()
+        return cls(rule=rule, messages=list(messages))
diff --git a/airflow/upgrade/rules/__init__.py 
b/airflow/upgrade/rules/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/airflow/upgrade/rules/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/airflow/upgrade/rules/base_rule.py 
b/airflow/upgrade/rules/base_rule.py
new file mode 100644
index 0000000..75ebe2f
--- /dev/null
+++ b/airflow/upgrade/rules/base_rule.py
@@ -0,0 +1,51 @@
+# 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 abc import ABCMeta, abstractmethod
+
+from six import add_metaclass
+
+RULES = []
+
+
+class BaseRuleMeta(ABCMeta):
+    def __new__(cls, clsname, bases, attrs):
+        clazz = super(BaseRuleMeta, cls).__new__(cls, clsname, bases, attrs)
+        if clsname != "BaseRule":
+            RULES.append(clazz)
+        return clazz
+
+
+@add_metaclass(BaseRuleMeta)
+class BaseRule(object):
+
+    @property
+    @abstractmethod
+    def title(self):
+        # type: () -> str
+        """Short one-line summary"""
+        pass
+
+    @property
+    @abstractmethod
+    def description(self):
+        # type: () -> str
+        """A long description explaining the problem in detail. This can be an 
entry from UPDATING.md file."""
+        pass
+
+    def check(self):
+        pass
diff --git a/airflow/upgrade/rules/conn_type_is_not_nullable.py 
b/airflow/upgrade/rules/conn_type_is_not_nullable.py
new file mode 100644
index 0000000..2ddd7c2
--- /dev/null
+++ b/airflow/upgrade/rules/conn_type_is_not_nullable.py
@@ -0,0 +1,42 @@
+# 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 absolute_import
+
+from airflow.models import Connection
+from airflow.upgrade.rules.base_rule import BaseRule
+from airflow.utils.db import provide_session
+
+
+class ConnTypeIsNotNullableRule(BaseRule):
+
+    title = "Connection.conn_type is not nullable"
+
+    description = """\
+The `conn_type` column in the `connection` table must contain content. 
Previously, this rule was \
+enforced by application logic, but was not enforced by the database schema.
+
+If you made any modifications to the table directly, make sure you don't have 
null in the conn_type column.\
+"""
+
+    @provide_session
+    def check(self, session=None):
+        invalid_connections = 
session.query(Connection).filter(Connection.conn_type.is_(None))
+        return (
+            'Connection<id={}", conn_id={}> have empty conn_type 
field.'.format(conn.id, conn.conn_id)
+            for conn in invalid_connections
+        )
diff --git a/airflow/utils/cli.py b/airflow/utils/cli.py
index 334c58f..9018407 100644
--- a/airflow/utils/cli.py
+++ b/airflow/utils/cli.py
@@ -26,9 +26,13 @@ import functools
 import getpass
 import json
 import socket
+import struct
 import sys
 from argparse import Namespace
 from datetime import datetime
+from fcntl import ioctl
+from os import environ
+from termios import TIOCGWINSZ
 
 from airflow.models import Log
 from airflow.utils import cli_action_loggers
@@ -137,3 +141,22 @@ def should_use_colors(args):
     if args.color == ColorMode.OFF:
         return False
     return is_terminal_support_colors()
+
+
+def get_terminal_size(fallback=(80, 20)):
+    """Return a tuple of (terminal height, terminal width)."""
+    try:
+        return struct.unpack('hhhh', ioctl(sys.__stdout__, TIOCGWINSZ, '\000' 
* 8))[0:2]
+    except IOError:
+        # when the output stream or init descriptor is not a tty, such
+        # as when when stdout is piped to another program
+        pass
+    try:
+        return int(environ.get('LINES')), int(environ.get('COLUMNS'))
+    except TypeError:
+        return fallback
+
+
+def header(text, fillchar):
+    rows, columns = get_terminal_size()
+    print(" {} ".format(text).center(columns, fillchar))
diff --git a/docs/conf.py b/docs/conf.py
index 101d050..d70dd69 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -216,6 +216,7 @@ exclude_patterns = [
     '_api/airflow/typing_compat',
     '_api/airflow/kubernetes',
     '_api/airflow/ti_deps',
+    '_api/airflow/upgrade',
     '_api/airflow/utils',
     '_api/airflow/version',
     '_api/airflow/www',
diff --git a/tests/upgrade/__init__.py b/tests/upgrade/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/upgrade/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/upgrade/rules/__init__.py b/tests/upgrade/rules/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/upgrade/rules/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/upgrade/rules/test_base_rule.py 
b/tests/upgrade/rules/test_base_rule.py
new file mode 100644
index 0000000..dbf47e3
--- /dev/null
+++ b/tests/upgrade/rules/test_base_rule.py
@@ -0,0 +1,26 @@
+# 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 airflow.upgrade.rules.base_rule import BaseRule, RULES
+
+
+class TestBaseRule:
+    def test_if_custom_rule_is_registered(self):
+        class CustomRule(BaseRule):
+            pass
+
+        assert CustomRule in list(RULES)
diff --git a/tests/upgrade/rules/test_conn_type_is_not_nullable.py 
b/tests/upgrade/rules/test_conn_type_is_not_nullable.py
new file mode 100644
index 0000000..53a14a5
--- /dev/null
+++ b/tests/upgrade/rules/test_conn_type_is_not_nullable.py
@@ -0,0 +1,40 @@
+# 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 airflow.models import Connection
+from airflow.upgrade.rules.conn_type_is_not_nullable import 
ConnTypeIsNotNullableRule
+from airflow.utils.db import create_session
+from tests.test_utils.db import clear_db_connections
+
+
+class TestConnTypeIsNotNullableRule:
+    def tearDown(self):
+        clear_db_connections()
+
+    def test_check(self):
+        rule = ConnTypeIsNotNullableRule()
+
+        assert isinstance(rule.description, str)
+        assert isinstance(rule.title, str)
+
+        with create_session() as session:
+            conn = Connection(conn_id="TestConnTypeIsNotNullableRule")
+            session.merge(conn)
+
+        msgs = rule.check(session=session)
+        assert [m for m in msgs if "TestConnTypeIsNotNullableRule" in m], \
+            "TestConnTypeIsNotNullableRule not in warning messages"
diff --git a/tests/upgrade/test_formattes.py b/tests/upgrade/test_formattes.py
new file mode 100644
index 0000000..0fc8f13
--- /dev/null
+++ b/tests/upgrade/test_formattes.py
@@ -0,0 +1,54 @@
+# 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.
+
+import json
+from tempfile import NamedTemporaryFile
+from tests.compat import mock
+
+import pytest
+
+from airflow.bin import cli
+from airflow.upgrade.rules.base_rule import BaseRule
+
+MESSAGES = ["msg1", "msg2"]
+
+
+class MockRule(BaseRule):
+    title = "title"
+    description = "description"
+
+    def check(self):
+        return MESSAGES
+
+
+class TestJSONFormatter:
+    @mock.patch("airflow.upgrade.checker.ALL_RULES", [MockRule()])
+    def test_output(self):
+        expected = [
+            {
+                "rule": MockRule.__name__,
+                "title": MockRule.title,
+                "messages": MESSAGES,
+            }
+        ]
+        parser = cli.CLIFactory.get_parser()
+        with NamedTemporaryFile("w+") as temp:
+            with pytest.raises(SystemExit):
+                cli.upgrade_check(parser.parse_args(['upgrade_check', '-s', 
temp.name]))
+            content = temp.read()
+
+        assert json.loads(content) == expected
diff --git a/tests/upgrade/test_problem.py b/tests/upgrade/test_problem.py
new file mode 100644
index 0000000..af2c5b9
--- /dev/null
+++ b/tests/upgrade/test_problem.py
@@ -0,0 +1,35 @@
+# 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 tests.compat import mock
+
+from airflow.upgrade.problem import RuleStatus
+
+
+class TestRuleStatus:
+    def test_is_success(self):
+        assert RuleStatus(rule=mock.MagicMock(), messages=[]).is_success is 
False
+        assert RuleStatus(rule=mock.MagicMock(), messages=["aaa"]).is_success 
is True
+
+    def test_rule_status_from_rule(self):
+        msgs = ["An interesting problem to solve"]
+        rule = mock.MagicMock()
+        rule.check.return_value = msgs
+
+        result = RuleStatus.from_rule(rule)
+        rule.check.assert_called_once_with()
+        assert result == RuleStatus(rule, msgs)

Reply via email to