EgorBaranovEnjoysTyping commented on code in PR #13470:
URL: https://github.com/apache/ignite/pull/13470#discussion_r3803761158


##########
modules/ducktests/tests/checks/utils/check_pause.py:
##########
@@ -0,0 +1,407 @@
+# 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.
+
+"""
+Checks demo breakpoints.
+"""
+
+import json
+import os
+import threading
+import time
+from types import SimpleNamespace
+
+import pytest
+
+from ignitetest.services.utils.path import IgnitePathAware
+from ignitetest.utils.pause import ALL, CONTINUE_ALL, ABORT, DemoPause, 
RUNNER_TIMEOUT_MARGIN_SEC, STATUS_JSON, \
+    STATUS_TXT, continue_file, parse_selector
+
+
+class FakeLogger:
+    """
+    Collects what a paused test would have logged.
+    """
+    def __init__(self):
+        self.messages = []
+
+    def info(self, msg):
+        """Records an info message."""
+        self.messages.append(msg)
+
+    def warn(self, msg):
+        """Records a warning."""
+        self.messages.append(msg)
+
+    debug = info
+    error = warn
+
+
+def _fake_nodes(*hostnames):
+    return [SimpleNamespace(account=SimpleNamespace(hostname=host, 
externally_routable_ip=host))
+            for host in hostnames]
+
+
+class FakeService:
+    """
+    Stands in for a non-Ignite service of the test registry, e.g. a zookeeper 
one: it carries
+    paths of its own, which the banner must not hand out for Ignite nodes.
+    """
+    log_dir = "/mnt/service/zk-logs"
+    config_file = "/mnt/service/zookeeper.properties"
+
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+
+class FakeIgniteService(IgnitePathAware):
+    """
+    Stands in for an Ignite service, with the real path layout behind it.
+    """
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+    @property
+    def product(self):
+        return "ignite-dev"
+
+    @property
+    def globals(self):
+        return {}
+
+
+class FakeRegistry:
+    """
+    Stands in for ducktape's ServiceRegistry, which is what a test hands the 
breakpoint: it is
+    iterable and nothing else, so a banner may not index it or ask it for a 
length.
+    """
+    def __init__(self, *services):
+        self._services = services
+
+    def __iter__(self):
+        return iter(self._services)
+
+
+def _pause(control_dir, started_at=None, runner_timeout_sec=None, 
**test_globals):
+    return DemoPause(FakeLogger(), test_globals, 
"check.CheckPause.check_something", control_dir=str(control_dir),
+                     started_at=started_at, 
runner_timeout_sec=runner_timeout_sec)
+
+
+def _read_published(control_dir, resume_with=None, timeout_sec=30):
+    """
+    Reads the published breakpoint while the test blocks on it, the way the 
host console does,
+    and optionally resumes it.
+
+    Polls for the file rather than reading it once after a fixed delay: a 
breakpoint that is
+    only held for a fraction of a second - which is what these checks hold 
them for - would
+    otherwise be a race against the machine the checks happen to run on.
+
+    :return: The dict that is filled in once the breakpoint has been 
published, and the reader
+             to join before reading it.
+    """
+    published = {}
+
+    def act():
+        deadline = time.monotonic() + timeout_sec
+
+        while time.monotonic() < deadline:
+            try:
+                with open(os.path.join(str(control_dir), STATUS_JSON), 
encoding="utf-8") as file:
+                    published.update(json.load(file))
+
+                break
+            except (OSError, ValueError):
+                time.sleep(.01)
+
+        if resume_with:
+            open(os.path.join(str(control_dir), resume_with), "w").close()
+
+    reader = threading.Thread(target=act, daemon=True)
+    reader.start()
+
+    return published, reader
+
+
+def _resume_with(control_dir, name, delay_sec=.05):
+    """
+    Creates a resume file from another thread, the way the host does while the 
test blocks.
+    """
+    timer = threading.Timer(delay_sec, lambda: 
open(os.path.join(str(control_dir), name), "w").close())
+    timer.daemon = True
+    timer.start()
+
+    return timer
+
+
+def check_selector_parsing():

Review Comment:
   I suppose word "validate" suites more for this method



##########
modules/ducktests/tests/checks/utils/check_pause.py:
##########
@@ -0,0 +1,407 @@
+# 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.
+
+"""
+Checks demo breakpoints.
+"""
+
+import json
+import os
+import threading
+import time
+from types import SimpleNamespace
+
+import pytest
+
+from ignitetest.services.utils.path import IgnitePathAware
+from ignitetest.utils.pause import ALL, CONTINUE_ALL, ABORT, DemoPause, 
RUNNER_TIMEOUT_MARGIN_SEC, STATUS_JSON, \
+    STATUS_TXT, continue_file, parse_selector
+
+
+class FakeLogger:
+    """
+    Collects what a paused test would have logged.
+    """
+    def __init__(self):
+        self.messages = []
+
+    def info(self, msg):
+        """Records an info message."""
+        self.messages.append(msg)
+
+    def warn(self, msg):
+        """Records a warning."""
+        self.messages.append(msg)
+
+    debug = info
+    error = warn
+
+
+def _fake_nodes(*hostnames):
+    return [SimpleNamespace(account=SimpleNamespace(hostname=host, 
externally_routable_ip=host))
+            for host in hostnames]
+
+
+class FakeService:
+    """
+    Stands in for a non-Ignite service of the test registry, e.g. a zookeeper 
one: it carries
+    paths of its own, which the banner must not hand out for Ignite nodes.
+    """
+    log_dir = "/mnt/service/zk-logs"
+    config_file = "/mnt/service/zookeeper.properties"
+
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+
+class FakeIgniteService(IgnitePathAware):
+    """
+    Stands in for an Ignite service, with the real path layout behind it.
+    """
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+    @property
+    def product(self):
+        return "ignite-dev"
+
+    @property
+    def globals(self):
+        return {}
+
+
+class FakeRegistry:
+    """
+    Stands in for ducktape's ServiceRegistry, which is what a test hands the 
breakpoint: it is
+    iterable and nothing else, so a banner may not index it or ask it for a 
length.
+    """
+    def __init__(self, *services):
+        self._services = services
+
+    def __iter__(self):
+        return iter(self._services)
+
+
+def _pause(control_dir, started_at=None, runner_timeout_sec=None, 
**test_globals):
+    return DemoPause(FakeLogger(), test_globals, 
"check.CheckPause.check_something", control_dir=str(control_dir),
+                     started_at=started_at, 
runner_timeout_sec=runner_timeout_sec)
+
+
+def _read_published(control_dir, resume_with=None, timeout_sec=30):
+    """
+    Reads the published breakpoint while the test blocks on it, the way the 
host console does,
+    and optionally resumes it.
+
+    Polls for the file rather than reading it once after a fixed delay: a 
breakpoint that is
+    only held for a fraction of a second - which is what these checks hold 
them for - would
+    otherwise be a race against the machine the checks happen to run on.
+
+    :return: The dict that is filled in once the breakpoint has been 
published, and the reader
+             to join before reading it.
+    """
+    published = {}
+
+    def act():

Review Comment:
   I think this inner method could be private method in module



##########
modules/ducktests/tests/checks/utils/check_pause.py:
##########
@@ -0,0 +1,407 @@
+# 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.
+
+"""
+Checks demo breakpoints.
+"""
+
+import json
+import os
+import threading
+import time
+from types import SimpleNamespace
+
+import pytest
+
+from ignitetest.services.utils.path import IgnitePathAware
+from ignitetest.utils.pause import ALL, CONTINUE_ALL, ABORT, DemoPause, 
RUNNER_TIMEOUT_MARGIN_SEC, STATUS_JSON, \
+    STATUS_TXT, continue_file, parse_selector
+
+
+class FakeLogger:

Review Comment:
   I think it's better to docompose this module, it seems to be huge. 1 class 
per module and it's own docs



##########
modules/ducktests/tests/checks/utils/check_pause.py:
##########
@@ -0,0 +1,407 @@
+# 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.
+
+"""
+Checks demo breakpoints.
+"""
+
+import json
+import os
+import threading
+import time
+from types import SimpleNamespace
+
+import pytest
+
+from ignitetest.services.utils.path import IgnitePathAware
+from ignitetest.utils.pause import ALL, CONTINUE_ALL, ABORT, DemoPause, 
RUNNER_TIMEOUT_MARGIN_SEC, STATUS_JSON, \
+    STATUS_TXT, continue_file, parse_selector
+
+
+class FakeLogger:
+    """
+    Collects what a paused test would have logged.
+    """
+    def __init__(self):
+        self.messages = []
+
+    def info(self, msg):
+        """Records an info message."""
+        self.messages.append(msg)
+
+    def warn(self, msg):
+        """Records a warning."""
+        self.messages.append(msg)
+
+    debug = info
+    error = warn
+
+
+def _fake_nodes(*hostnames):
+    return [SimpleNamespace(account=SimpleNamespace(hostname=host, 
externally_routable_ip=host))
+            for host in hostnames]
+
+
+class FakeService:
+    """
+    Stands in for a non-Ignite service of the test registry, e.g. a zookeeper 
one: it carries
+    paths of its own, which the banner must not hand out for Ignite nodes.
+    """
+    log_dir = "/mnt/service/zk-logs"
+    config_file = "/mnt/service/zookeeper.properties"
+
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+
+class FakeIgniteService(IgnitePathAware):
+    """
+    Stands in for an Ignite service, with the real path layout behind it.
+    """
+    def __init__(self, *hostnames):
+        self.nodes = _fake_nodes(*hostnames)
+
+    def who_am_i(self, node):
+        """Names the node the way a ducktape service does."""
+        return f"{self.__class__.__name__}-{node.account.hostname}"
+
+    @property
+    def product(self):
+        return "ignite-dev"
+
+    @property
+    def globals(self):
+        return {}
+
+
+class FakeRegistry:
+    """
+    Stands in for ducktape's ServiceRegistry, which is what a test hands the 
breakpoint: it is
+    iterable and nothing else, so a banner may not index it or ask it for a 
length.
+    """
+    def __init__(self, *services):
+        self._services = services
+
+    def __iter__(self):
+        return iter(self._services)
+
+
+def _pause(control_dir, started_at=None, runner_timeout_sec=None, 
**test_globals):
+    return DemoPause(FakeLogger(), test_globals, 
"check.CheckPause.check_something", control_dir=str(control_dir),
+                     started_at=started_at, 
runner_timeout_sec=runner_timeout_sec)
+
+
+def _read_published(control_dir, resume_with=None, timeout_sec=30):
+    """
+    Reads the published breakpoint while the test blocks on it, the way the 
host console does,
+    and optionally resumes it.
+
+    Polls for the file rather than reading it once after a fixed delay: a 
breakpoint that is
+    only held for a fraction of a second - which is what these checks hold 
them for - would
+    otherwise be a race against the machine the checks happen to run on.
+
+    :return: The dict that is filled in once the breakpoint has been 
published, and the reader
+             to join before reading it.
+    """
+    published = {}
+
+    def act():
+        deadline = time.monotonic() + timeout_sec
+
+        while time.monotonic() < deadline:
+            try:
+                with open(os.path.join(str(control_dir), STATUS_JSON), 
encoding="utf-8") as file:
+                    published.update(json.load(file))
+
+                break
+            except (OSError, ValueError):
+                time.sleep(.01)
+
+        if resume_with:
+            open(os.path.join(str(control_dir), resume_with), "w").close()
+
+    reader = threading.Thread(target=act, daemon=True)
+    reader.start()
+
+    return published, reader
+
+
+def _resume_with(control_dir, name, delay_sec=.05):
+    """
+    Creates a resume file from another thread, the way the host does while the 
test blocks.
+    """
+    timer = threading.Timer(delay_sec, lambda: 
open(os.path.join(str(control_dir), name), "w").close())
+    timer.daemon = True
+    timer.start()
+
+    return timer
+
+
+def check_selector_parsing():

Review Comment:
   Or, I can't see all those methods are unit tests (how to run it)



##########
modules/ducktests/tests/docker/demo_console.py:
##########
@@ -0,0 +1,220 @@
+# 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.
+
+"""
+Host side of the demo breakpoints - run it in a second terminal, next to the 
one running
+``run_tests.sh``, when a test is started with the ``demo_pause`` global:
+
+    ./docker/run_tests.sh -gj '{"demo_pause": "*"}' -t 
./ignitetest/tests/<some_test.py>
+
+    python docker/demo_console.py
+
+Ducktape runs the test with stdin on /dev/null inside the ``ducker01`` 
container, so this is
+where the keyboard lives. The console itself is deliberately dumb: the test 
renders the
+banner and this only prints it and writes back a resume file. Everything it 
does can be done
+by hand instead - ``cat .ducktests-demo/paused.txt``, then ``touch 
.ducktests-demo/continue-3``.
+
+Standard library only: it runs on the host, outside the ducktests virtualenv.
+"""
+
+import argparse
+import importlib.util
+import json
+import os
+import sys
+import time
+
+# Reach into the framework for the protocol constants rather than restating 
them. The host
+# has no ducktape and no installed ignitetest, so the module is loaded by 
path: importing
+# ignitetest.utils.pause would pull in the package __init__ chain and its 
ducktape imports.
+_TESTS_DIR = 
os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 
os.pardir))
+_PAUSE_PY = os.path.join(_TESTS_DIR, "ignitetest", "utils", "pause.py")
+
+_SPEC = importlib.util.spec_from_file_location("ignitetest_pause", _PAUSE_PY)
+pause = importlib.util.module_from_spec(_SPEC)
+_SPEC.loader.exec_module(pause)
+
+POLL_SEC = .3
+
+KEYS = """
+  [Enter] continue        [c] continue, skipping the rest        [a] abort the 
test
+  [q] leave the console (the test stays paused)
+"""
+
+
+def read_status(control_dir):
+    """
+    :return: The published breakpoint, or None when the scenario is not 
paused. A missing or
+             half written file simply reads as "not paused" and is retried.
+    """
+    path = os.path.join(control_dir, pause.STATUS_JSON)
+
+    try:
+        with open(path, encoding="utf-8") as file:
+            return json.load(file)
+    except (OSError, ValueError):
+        return None
+
+
+def breakpoint_key(status):
+    """
+    :return: What identifies the published breakpoint. Not the sequence number 
on its own:
+             that one is per test, so it restarts at 1 for every test of a 
session, and a
+             run that died while paused leaves behind a banner numbered like a 
live one.
+    """
+    return status.get("run"), status.get("seq")
+
+
+def clear_stale(control_dir):
+    """
+    Removes resume files left behind by an earlier run, which would otherwise 
skip the first
+    breakpoint of this one. The test clears them too, on its side, at its 
first breakpoint.
+
+    Only ever done while nothing is published: a console is just as likely to 
be started
+    against a test that is already holding a breakpoint, and a resume file 
that was written
+    for that one - by hand, or by a console that has just been closed - is the 
host's answer
+    to it rather than a leftover. The published breakpoint itself is left 
alone either way,
+    a stale banner is told apart by its run id.
+
+    :return: Whether the sweep was performed.
+    """
+    if not os.path.isdir(control_dir):
+        return True
+
+    if read_status(control_dir) is not None:
+        return False
+
+    for name in os.listdir(control_dir):
+        if name.startswith(pause.CONTINUE_PREFIX) or name == pause.ABORT:
+            try:
+                os.remove(os.path.join(control_dir, name))
+            except OSError:
+                pass
+
+    return True
+
+
+def resume(control_dir, name):
+    """
+    Writes a resume file. The test consumes and removes it.
+    """
+    with open(os.path.join(control_dir, name), "w", encoding="utf-8") as file:
+        file.write("")
+
+
+def prompt(control_dir, seq):
+    """
+    Asks what to do with the breakpoint that is currently published.
+
+    :return: False when the console should stop, True to wait for the next 
breakpoint.
+    """
+    while True:
+        try:
+            answer = input("  > ").strip().lower()
+        except EOFError:
+            return False
+
+        if answer in ("", "n", "next"):
+            resume(control_dir, pause.continue_file(seq))
+
+            return True
+
+        if answer in ("c", "continue", "all"):
+            resume(control_dir, pause.CONTINUE_ALL)
+
+            print("  continuing, remaining breakpoints skipped")
+
+            return False
+
+        if answer in ("a", "abort"):
+            resume(control_dir, pause.ABORT)
+
+            print("  aborting the test")
+
+            return False
+
+        if answer in ("q", "quit", "exit"):
+            print(f"  leaving the test paused, resume it with:\n"
+                  f"    touch {os.path.join(control_dir, 
pause.continue_file(seq))}")
+
+            return False
+
+        print(KEYS)
+
+
+def main():
+    """
+    Waits for breakpoints and drives them until the test is resumed for good.
+    """
+    parser = argparse.ArgumentParser(description="Drives the ducktests demo 
breakpoints.")
+    parser.add_argument("-d", "--control-dir", 
default=pause.default_control_dir(),
+                        help="control directory shared with the test, defaults 
to "
+                             f"<repository root>/{pause.CONTROL_DIR_NAME}")
+
+    args = parser.parse_args()
+    control_dir = args.control_dir
+
+    swept = clear_stale(control_dir)
+
+    print(f"Demo console, watching {control_dir}")

Review Comment:
   Isn't logger better here?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to