#!/usr/bin/env python3

"""Regression tests for the jobserver_pool.py script."""

import os
import re
import platform
import subprocess
import sys
import tempfile
import unittest
import typing as T

_SCRIPT_DIR = os.path.dirname(__file__)
_JOBSERVER_SCRIPT = os.path.join(_SCRIPT_DIR, "jobserver_pool.py")
_JOBSERVER_CMD = [sys.executable, _JOBSERVER_SCRIPT]

_IS_WINDOWS = sys.platform == "win32"

# This is only here to avoid depending on the non-standard
# scanf package which does the job properly :-)


def _simple_scanf(pattern: str, input: str) -> T.Sequence[T.Any]:
    """Extract values from input using a scanf-like pattern.

    This is very basic and only used to avoid depending on the
    non-standard scanf package which does the job properly.
    Only supports %d, %s and %%, does not support any fancy
    escaping.
    """
    re_pattern = ""
    groups = ""
    from_pos = 0

    # Just in case.
    assert "." not in pattern, f"Dots in pattern not supported."
    assert "?" not in pattern, f"Question marks in pattern not supported."

    while True:
        next_percent = pattern.find("%", from_pos)
        if next_percent < 0 or next_percent + 1 >= len(pattern):
            re_pattern += pattern[from_pos:]
            break

        re_pattern += pattern[from_pos:next_percent]

        from_pos = next_percent + 2
        formatter = pattern[next_percent + 1]
        if formatter == "%":
            re_pattern += "%"
        elif formatter == "d":
            groups += formatter
            re_pattern += "(\\d+)"
        elif formatter == "s":
            groups += formatter
            re_pattern += "(\\S+)"
        else:
            assert False, f"Unsupported scanf formatter: %{formatter}"

    m = re.match(re_pattern, input)
    if not m:
        return None

    result = []
    for group_index, formatter in enumerate(groups, start=1):
        if formatter == "d":
            result.append(int(m.group(group_index)))
        elif formatter == "s":
            result.append(m.group(group_index))
        else:
            assert False, f"Unsupported formatter {formatter}"

    return result


class JobserverPool(unittest.TestCase):
    def _run_jobserver_echo_MAKEFLAGS(
        self, cmd_args_prefix
    ) -> "subprocess.CompletedProcess[str]":
        if _IS_WINDOWS:
            cmd_args = cmd_args_prefix + ["cmd.exe", "/c", "echo %MAKEFLAGS%"]
        else:
            cmd_args = cmd_args_prefix + ["sh", "-c", 'echo "$MAKEFLAGS"']

        ret = subprocess.run(
            cmd_args,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        ret.check_returncode()
        return ret

    def _test_echo_MAKEFLAGS(self, cmd_args_prefix, expected_core_count: int):
        ret = self._run_jobserver_echo_MAKEFLAGS(cmd_args_prefix)
        makeflags = ret.stdout.rstrip()

        if expected_core_count == 0:
            if _IS_WINDOWS:
                # On Windows, echo %FOO% prints "%FOO%" if FOO is not defined!
                self.assertEqual(makeflags.strip(), "%MAKEFLAGS%")
            else:
                self.assertEqual(makeflags.strip(), "")

        else:  # expected_core_count > 0
            if _IS_WINDOWS:
                expected_format = " -j%d --jobserver-auth=%s"
            else:
                expected_format = " -j%d --jobserver-auth=fifo:%s"

            m = _simple_scanf(expected_format, makeflags)
            self.assertTrue(
                m,
                f"Invalid MAKEFLAGS value, expected format [{expected_format}], got: [{makeflags}]",
            )

            if _IS_WINDOWS:
                sem_name = m[1]
                self.assertEqual(
                    sem_name,
                    "jobserver_pool",
                    f"Invalid semaphore name in MAKEFLAGS value [{makeflags}]",
                )
            else:
                fifo_name = os.path.basename(m[1])
                self.assertEqual(
                    fifo_name,
                    "jobserver_pool",
                    f"Invalid fifo name in MAKEFLAGS value [{makeflags}]",
                )

            core_count = m[0]
            self.assertEqual(
                core_count,
                expected_core_count,
                f"Invalid core count {core_count}, expected {expected_core_count}",
            )

    def test_MAKEFLAGS_default(self):
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD, os.cpu_count())

    def test_MAKEFLAGS_with_10_jobs(self):
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["-j10"], 10)
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["--jobs=10"], 10)
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["--jobs", "10"], 10)

    def test_MAKEFLAGS_with_no_jobs(self):
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["-j0"], 0)
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["--jobs=0"], 0)
        self._test_echo_MAKEFLAGS(_JOBSERVER_CMD + ["--jobs", "0"], 0)

    @unittest.skipIf(_IS_WINDOWS, "--fifo is not supported on Windows")
    def test_MAKEFLAGS_with_fifo(self):
        fifo_name = "test_fifo"
        fifo_path = os.path.abspath(fifo_name)
        ret = self._run_jobserver_echo_MAKEFLAGS(
            _JOBSERVER_CMD + ["-j10", "--fifo", fifo_name]
        )
        makeflags = ret.stdout.rstrip()
        self.assertEqual(makeflags, " -j10 --jobserver-auth=fifo:" + fifo_path)

    @unittest.skipIf(not _IS_WINDOWS, "--name is not supported on Posix")
    def test_MAKEFLAGS_with_name(self):
        sem_name = "test_semaphore"
        ret = self._run_jobserver_echo_MAKEFLAGS(
            _JOBSERVER_CMD + ["-j10", "--name", sem_name]
        )
        makeflags = ret.stdout.rstrip()
        self.assertEqual(makeflags, " -j10 --jobserver-auth=" + sem_name)


if __name__ == "__main__":
    unittest.main()
