<commit_before># Copyright 2019 The Pigweed Authors
#
# Licensed 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
#
#     https://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.
"""Rebuild every time a file is chanegd."""

import argparse
import enum
import glob
import logging
import os
import pathlib
import subprocess
import sys
import time

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.utils import has_attribute
from watchdog.utils import unicode_paths

import pw_cli.color
import pw_cli.plugins

_COLOR = pw_cli.color.colors()
_LOG = logging.getLogger(__name__)

_BUILD_MESSAGE = """
  ██████╗ ██╗   ██╗██╗██╗     ██████╗
  ██╔══██╗██║   ██║██║██║     ██╔══██╗
  ██████╔╝██║   ██║██║██║     ██║  ██║
  ██╔══██╗██║   ██║██║██║     ██║  ██║
  ██████╔╝╚██████╔╝██║███████╗██████╔╝
  ╚═════╝  ╚═════╝ ╚═╝╚══════╝╚═════╝
"""

_PASS_MESSAGE = """
  ██████╗  █████╗ ███████╗███████╗██╗
  ██╔══██╗██╔══██╗██╔════╝██╔════╝██║
  ██████╔╝███████║███████╗███████╗██║
  ██╔═══╝ ██╔══██║╚════██║╚════██║╚═╝
  ██║     ██║  ██║███████║███████║██╗
  ╚═╝     ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝
"""

# Pick a visually-distinct font from "PASS" to ensure that readers can't
# possibly mistake the difference between the two states.
_FAIL_MESSAGE = """
   ▄██████▒░▄▄▄       ██▓  ░██▓
  ▓█▓     ░▒████▄    ▓██▒  ░▓██▒
  ▒████▒   ░▒█▀  ▀█▄  ▒██▒ ▒██░
  ░▓█▒    ░░██▄▄▄▄██ ░██░  ▒██░
  ░▒█░      ▓█   ▓██▒░██░░ ████████▒
   ▒█░      ▒▒   ▓▒█░░▓  ░  ▒░▓  ░
   ░▒        ▒   ▒▒ ░ ▒ ░░  ░ ▒  ░
   ░ ░       ░   ▒    ▒ ░   ░ ░
                 ░  ░ ░       ░  ░
"""


class _State(enum.Enum):
    WAITING_FOR_FILE_CHANGE_EVENT = 1
    COOLDOWN_IGNORING_EVENTS = 2


# TODO(keir): Figure out a better strategy for exiting. The problem with the
# watcher is that doing a "clean exit" is slow. However, by directly exiting,
# we remove the possibility of the wrapper script doing anything on exit.
def _die(*args):
    _LOG.fatal(*args)
    sys.exit(1)


# pylint: disable=logging-format-interpolation


class PigweedBuildWatcher(FileSystemEventHandler):
    """Process filesystem events and launch builds if necessary."""
    def __init__(self,
                 patterns=None,
                 ignore_patterns=None,
                 case_sensitive=False,
                 build_dirs=None):
        super().__init__()

        self.patterns = patterns
        self.ignore_patterns = ignore_patterns
        self.case_sensitive = case_sensitive
        self.state = _State.WAITING_FOR_FILE_CHANGE_EVENT
        self.build_dirs = build_dirs or []
        self.cooldown_finish_time = None

    def path_matches(self, raw_path):
        """Returns true if path matches according to the watcher patterns"""
        modified_path = pathlib.Path(raw_path).resolve()

        # Check for modifications inside the build directory outputs, and skip
        # them. Ideally these events would never hit the watcher, but
        # selectively watching directories at the OS level is not trivial due
        # to limitations of the watchdog module.
        for build_dir in self.build_dirs:
            resolved_build_dir = pathlib.Path(build_dir).resolve()
            try:
                modified_path.relative_to(resolved_build_dir)
                # If no ValueError is raised by the .relative_to() call, then
                # this file is inside the build directory; so skip it.
                return False
            except ValueError:
                # Otherwise, the file isn't in the build directory, so run the
                # normal pattern checks below.
                pass

        return ((not any(modified_path.match(x) for x in self.ignore_patterns))
                and any(modified_path.match(x) for x in self.patterns))

    def dispatch(self, event):
        # There isn't any point in triggering builds on new directory creation.
        # It's the creation or modification of files that indicate something
        # meaningful enough changed for a build.
        if event.is_directory:
            return

        # Collect paths of interest from the event.
        paths = []
        if has_attribute(event, 'dest_path'):
            paths.append(unicode_paths.decode(event.dest_path))
        if event.src_path:
            paths.append(unicode_paths.decode(event.src_path))
        for path in paths:
            _LOG.debug('File event: %s', path)

        # Check for matching paths among the one or two in the event.
        matching_path = None
        for path in paths:
            if self.path_matches(path):
                _LOG.debug('Detected event: %s', path)
                matching_path = path
                break

        if matching_path:
            self.handle_matched_event(matching_path)

    def run_builds(self, matching_path):
        """Run all the builds in serial and capture pass/fail for each."""

        # Clear the screen and show a banner indicating the build is starting.
        print("\033c", end="")  # TODO(pwbug/38): Not Windows compatible.
        print(_COLOR.magenta(_BUILD_MESSAGE))
        _LOG.info('Change detected: %s', matching_path)

        builds_succeeded = []
        num_builds = len(self.build_dirs)
        _LOG.info(f'Starting build with {num_builds} directories')
        for i, build_dir in enumerate(self.build_dirs, 1):
            _LOG.info(f'[{i}/{num_builds}] Starting build: {build_dir}')

            # Run the build. Put a blank before/after for visual separation.
            print()
            result = subprocess.run(['ninja', '-C', build_dir])
            print()

            build_ok = (result.returncode == 0)
            if build_ok:
                tag = '(OK)'
            else:
                tag = '(FAIL)'
            _LOG.info(f'[{i}/{num_builds}] Finished build: {build_dir} {tag}')
            builds_succeeded.append(build_ok)

        if all(builds_succeeded):
            _LOG.info('Finished; all successful.')<commit_msg>pw_watch: Set log level for build result log line<commit_after># Copyright 2019 The Pigweed Authors
#
# Licensed 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
#
#     https://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.
"""Rebuild every time a file is chanegd."""

import argparse
import enum
import glob
import logging
import os
import pathlib
import subprocess
import sys
import time

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.utils import has_attribute
from watchdog.utils import unicode_paths

import pw_cli.color
import pw_cli.plugins

_COLOR = pw_cli.color.colors()
_LOG = logging.getLogger(__name__)

_BUILD_MESSAGE = """
  ██████╗ ██╗   ██╗██╗██╗     ██████╗
  ██╔══██╗██║   ██║██║██║     ██╔══██╗
  ██████╔╝██║   ██║██║██║     ██║  ██║
  ██╔══██╗██║   ██║██║██║     ██║  ██║
  ██████╔╝╚██████╔╝██║███████╗██████╔╝
  ╚═════╝  ╚═════╝ ╚═╝╚══════╝╚═════╝
"""

_PASS_MESSAGE = """
  ██████╗  █████╗ ███████╗███████╗██╗
  ██╔══██╗██╔══██╗██╔════╝██╔════╝██║
  ██████╔╝███████║███████╗███████╗██║
  ██╔═══╝ ██╔══██║╚════██║╚════██║╚═╝
  ██║     ██║  ██║███████║███████║██╗
  ╚═╝     ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝
"""

# Pick a visually-distinct font from "PASS" to ensure that readers can't
# possibly mistake the difference between the two states.
_FAIL_MESSAGE = """
   ▄██████▒░▄▄▄       ██▓  ░██▓
  ▓█▓     ░▒████▄    ▓██▒  ░▓██▒
  ▒████▒   ░▒█▀  ▀█▄  ▒██▒ ▒██░
  ░▓█▒    ░░██▄▄▄▄██ ░██░  ▒██░
  ░▒█░      ▓█   ▓██▒░██░░ ████████▒
   ▒█░      ▒▒   ▓▒█░░▓  ░  ▒░▓  ░
   ░▒        ▒   ▒▒ ░ ▒ ░░  ░ ▒  ░
   ░ ░       ░   ▒    ▒ ░   ░ ░
                 ░  ░ ░       ░  ░
"""


class _State(enum.Enum):
    WAITING_FOR_FILE_CHANGE_EVENT = 1
    COOLDOWN_IGNORING_EVENTS = 2


# TODO(keir): Figure out a better strategy for exiting. The problem with the
# watcher is that doing a "clean exit" is slow. However, by directly exiting,
# we remove the possibility of the wrapper script doing anything on exit.
def _die(*args):
    _LOG.fatal(*args)
    sys.exit(1)


# pylint: disable=logging-format-interpolation


class PigweedBuildWatcher(FileSystemEventHandler):
    """Process filesystem events and launch builds if necessary."""
    def __init__(self,
                 patterns=None,
                 ignore_patterns=None,
                 case_sensitive=False,
                 build_dirs=None):
        super().__init__()

        self.patterns = patterns
        self.ignore_patterns = ignore_patterns
        self.case_sensitive = case_sensitive
        self.state = _State.WAITING_FOR_FILE_CHANGE_EVENT
        self.build_dirs = build_dirs or []
        self.cooldown_finish_time = None

    def path_matches(self, raw_path):
        """Returns true if path matches according to the watcher patterns"""
        modified_path = pathlib.Path(raw_path).resolve()

        # Check for modifications inside the build directory outputs, and skip
        # them. Ideally these events would never hit the watcher, but
        # selectively watching directories at the OS level is not trivial due
        # to limitations of the watchdog module.
        for build_dir in self.build_dirs:
            resolved_build_dir = pathlib.Path(build_dir).resolve()
            try:
                modified_path.relative_to(resolved_build_dir)
                # If no ValueError is raised by the .relative_to() call, then
                # this file is inside the build directory; so skip it.
                return False
            except ValueError:
                # Otherwise, the file isn't in the build directory, so run the
                # normal pattern checks below.
                pass

        return ((not any(modified_path.match(x) for x in self.ignore_patterns))
                and any(modified_path.match(x) for x in self.patterns))

    def dispatch(self, event):
        # There isn't any point in triggering builds on new directory creation.
        # It's the creation or modification of files that indicate something
        # meaningful enough changed for a build.
        if event.is_directory:
            return

        # Collect paths of interest from the event.
        paths = []
        if has_attribute(event, 'dest_path'):
            paths.append(unicode_paths.decode(event.dest_path))
        if event.src_path:
            paths.append(unicode_paths.decode(event.src_path))
        for path in paths:
            _LOG.debug('File event: %s', path)

        # Check for matching paths among the one or two in the event.
        matching_path = None
        for path in paths:
            if self.path_matches(path):
                _LOG.debug('Detected event: %s', path)
                matching_path = path
                break

        if matching_path:
            self.handle_matched_event(matching_path)

    def run_builds(self, matching_path):
        """Run all the builds in serial and capture pass/fail for each."""

        # Clear the screen and show a banner indicating the build is starting.
        print("\033c", end="")  # TODO(pwbug/38): Not Windows compatible.
        print(_COLOR.magenta(_BUILD_MESSAGE))
        _LOG.info('Change detected: %s', matching_path)

        builds_succeeded = []
        num_builds = len(self.build_dirs)
        _LOG.info(f'Starting build with {num_builds} directories')
        for i, build_dir in enumerate(self.build_dirs, 1):
            _LOG.info(f'[{i}/{num_builds}] Starting build: {build_dir}')

            # Run the build. Put a blank before/after for visual separation.
            print()
            result = subprocess.run(['ninja', '-C', build_dir])
            print()

            build_ok = (result.returncode == 0)
            if build_ok:
                level = logging.INFO
                tag = '(OK)'
            else:
                level = logging.ERROR
                tag = '(FAIL)'
            _LOG.log(level,
                     f'[{i}/{num_builds}] Finished build: {build_dir} {tag}')
            builds_succeeded.append(build_ok)

        if all(builds_succeeded):
            _LOG.info('Finished; all successful.')
{'fraction_non_alphanumeric': 0.14981389578163773, 'fraction_numerical': 0.002791563275434243, 'mean_word_length': 2.4585679806918743, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1520386', 'n_tokens_mistral': 4323, 'n_tokens_neox': 4278, 'n_words': 1222}