diff options
| -rw-r--r-- | challenge-286/ppentchev/README | 217 | ||||
| -rw-r--r-- | challenge-286/ppentchev/blog.txt | 1 | ||||
| -rw-r--r-- | challenge-286/ppentchev/docs/index.md | 41 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/README.md | 8 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/pyproject.toml | 19 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/requirements/ruff.txt | 4 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/ruff-base.toml | 39 | ||||
| -rwxr-xr-x | challenge-286/ppentchev/python/scripts/ch-1.py | 39 | ||||
| -rwxr-xr-x | challenge-286/ppentchev/python/scripts/ch-2.py | 109 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/tox.ini | 71 | ||||
| -rw-r--r-- | challenge-286/ppentchev/tests/01-perl-ch-1.t | 2 | ||||
| -rw-r--r-- | challenge-286/ppentchev/tests/02-perl-ch-2.t | 4 | ||||
| -rw-r--r-- | challenge-286/ppentchev/tests/03-python-ch-1.t | 31 | ||||
| -rw-r--r-- | challenge-286/ppentchev/tests/04-python-ch-2.t | 47 | ||||
| -rw-r--r-- | challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm | 31 |
15 files changed, 569 insertions, 94 deletions
diff --git a/challenge-286/ppentchev/README b/challenge-286/ppentchev/README index 1ceb3b34df..aadceb51f2 100644 --- a/challenge-286/ppentchev/README +++ b/challenge-286/ppentchev/README @@ -3,107 +3,170 @@ SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> SPDX-License-Identifier: BSD-2-Clause --> -# Parse a mini-language for selecting objects by tag or name +# Peter Pentchev's solutions to PWC 286 -\[[Home][ringlet-parse-stages] | [GitLab][gitlab] | [PyPI][pypi] | [ReadTheDocs][readthedocs]\] +\[[Home][ringlet-home] | [GitHub][github] ## Overview -The `parse-stages` Python library may be used by other tools to group -objects (e.g. [Tox] or [Nox] test environments) for step-by-step processing -(e.g. running some tests in parallel, then only running others if the first -group passes). +This directory contains Peter Pentchev's solutions to the two tasks in +the [Perl & Raku Weekly Challenge 286][pwc-286]. + +## General remarks + +### Task 1: Self Spammer + +Most programming languages have (different or the same) string-processing +functions that can: + +- read a file and split it into lines at the same time +- split the whole contents read from a file into lines +- split a string by some commonly-accepted set of whitespace characters + +The tricky part here is finding the program source. +In most of the so-called "interpreted" languages, e.g. Perl, Python, Ruby et al, +there is either a file-global variable or a function that one can call to +obtain the path to the file containing this particular source code line +(note that I put "interpreted" in quotes, since it is common for these languages to +do a precompilation phase to some intermediate representation, e.g. byte-code). +For compiled languages such as Rust, Go, C, C++, etc., this part may be a bit +more difficult since the end result does not generally include the program source code, +so this part may have to depend on some specifics of the build environment and +only be capable to run directly from a build directory near to its source code. + +### Task 2: Order Game + +There are several approaches to solving this problem: -The language used by the library is described in the -[_Grouping stages_](language.md#grouping-stages-for-step-by-step-execution) -section. +- trivial: take a pair of numbers, flip a "do we want the smaller or the larger one" flag, + produce the corresponding result, move on to the next pair +- keep this information in an object, feed it numbers one by one or in pairs, let it + update its inner state (or, if it is an immutable object, let it produce a new one with + the inner state updated) +- keep this information in an object, feed it number by number or the whole sequence, + have it work as an iterator that produces numbers for the programming language's + constructs to consume -[tox]: https://tox.wiki/en/latest/ "The tox automation project" -[nox]: https://nox.thea.codes/en/stable/ "The nox flexible automation tool" +Some languages have built-in constructs for dealing with iterators, others can +handle immutable objects easily and efficiently, so the approach depends on +the programming language. -## Installation +## Running the test suite -A program that uses the `parse-stages` library should specify it in -its list of requirements, e.g. using [PEP508][pep508] syntax: +The `tests/` subdirectory contains a Perl test suite that outputs TAP, so that +it can be run using `prove tests` (or, for the more adventurous, `prove -v tests`). + +The Python implementation has its own set of static checkers (linters). +Those can be run from the `python/` directory using Tox 4.x - either directly +(`tox run-parallel`) or using [the tox-stages tool][python-tox-stages] +(`tox-stages run`). - parse-stages >= 0.1.9, < 0.2 +### Task 1: Self Spammer -[pep508]: https://peps.python.org/pep-0508/ "PEP 508 – Dependency specification for Python Software Packages" +This task does not involve any input data (except for the program source itself), +so the programs are executed without any parameters. -## Parsing a stage specification +The `PWCTest::Ch286` module in `tests/lib/` defines a `test_self_spammer` function that +runs the specified program, checks whether it output a single line containing a single word, and +then looks for that word in the specified source file (the program itself by default). +Of course, for the Perl implementation there is a bit of a chicken-and-egg problem, since +the test itself must implement the same algorithm to check whether the word that +the program output is actually present as a separate word in the source file. +I played around a bit with the idea of using an external `grep` program to look for +the word, but it turns out that at least GNU grep has some... idionsyncrasies regarding +character sequences like `\<`, `\>`, `'')`, etc, so the output of Perl's `quotemeta` +function was not suitable for feeding to `grep` directly. +Eh, let's hope my solution is correct anyway :) -The `parse_spec()` function parses a string specification into -a [BoolExpr][parse_stages.BoolExpr] object that may later be used to -select matching objects (e.g. test environments). +The `tests/03-python-ch-1.t` test first looks for a suitable Python 3 interpreter. +If the `PYTHON3` environment variable is defined, it is used as the name or path of +the Python 3 interpreter; oterhwise, `python3` is used. +The test tries to run the Python 3 interpreter for a `pass` command; if that fails, +the tests are skipped. -``` py - e_check = parse_stages.parse_spec("@check") - e_check_quick = parse_stages.parse_spec("@check and @quick") - e_check_no_ruff = parse_stages.parse_spec("@check and not ruff") - - specs = [(spec, parse_stages.parse_spec(spec)) for spec in args.stage_specs] -``` +### Task 2: Order Game -## Check whether an object matches a parsed specification +This task takes some input, so there are two ways of running the program: -The `parse-stages` library provides two base dataclasses for objects that -may be matched against parsed expressions: -[TaggedFrozen][parse_stages.TaggedFrozen] and [Tagged][parse_stages.Tagged]. -Both classes have the same members: +- if the `PWC_FROM_STDIN` environment variable is not set to the exact value `1`, + the program runs the three sequences given as examples, and produces three numbers on + its standard output stream, each one on a line by itself. + In other words, the program must output `1\n0\n2\n` exactly. +- if the `PWC_FROM_STDIN` environment variable is set, the program reads a single line of + text from its standard input, treats it as a sequence of decimal integers separated by + one or more whitespace characters, runs the order game on that sequence, and produces + a single number on a line by itself on its standard output stream. -- [name][parse_stages.TaggedFrozen.name]: a string -- [tags][parse_stages.TaggedFrozen.tags]: a list of strings -- [get_keyword_haystacks()][parse_stages.TaggedFrozen.get_keyword_haystacks]: - a method that returns a list of strings, - `self.name` unless overridden +The `PWCTest::Ch286` module in `tests/lib/` defines a `test_order_game_default` function that +runs a program with `PWC_FROM_STDIN` unset and expects the exact output, and also +a `test_order_game` function that runs a series of tests with different sequences, +each time running the program with `PWC_FROM_STDIN` set to 1 and feeding it the sequence. -When a `BoolExpr` object's [evaluate()][parse_stages.BoolExpr.evaluate] -method is called for a specific -`TaggedFrozen` or `Tagged` object, it checks whether the specification -matches the tags and keywords defined for this object. Tags are matched -exactly, while a keyword is considered to match if it is contained in -the checked string; e.g. `pep` would match both `pep8` and `exp_pep563`, -while `@black` would not match a `black-reformat` tag. +If the implementation in any language should provide more than one method, then +the program should honor the `PWC_METHOD` environment variable. +The value "0" indicates the use of the most natural method for the language, +the value "1" indicates the use of an alternative method, and if there are more than two, +then the values "2", "3", etc, are used to select them. +If `PWC_METHOD` is set to a non-numeric value or to a value that is higher than +the index of the last supported methods, it is ignored and the program proceeds as if +`PWC_METHOD` was set to "0". -The `get_keyword_haystacks()` method returns the strings to look in for -matching keywords. By default, it only returns the `name` field; -however, it may be extended, e.g. for Nox sessions it may also return -the name of the Python function that implements the session, for test -classes with methods it may return the class name and the method name, etc. +The `tests/02-perl-ch-2.t` test runs these functions on the Perl implementation and +produces TAP output suitable for the `prove` tool. + +The `tests/03-python-ch-2.t` test first looks for a suitable Python 3 interpreter. +If the `PYTHON3` environment variable is defined, it is used as the name or path of +the Python 3 interpreter; oterhwise, `python3` is used. +The test tries to run the Python 3 interpreter for a `pass` command; if that fails, +the tests are skipped. + +## Implementation details + +### Task 1: Self Spammer + +#### Perl + +We use the [FindBin core module][perl-findbin] and its `$Bin` and `$Script` variables to +figure out where the program source is. + +#### Python -``` py - # Obtain a list (okay, a dictionary) of test environments in some way - tox_envs = get_tox_environments() # {"ruff": {"tags": ["check", "quick"]}, ...} +We use Python's built-in `__file__` pseudoglobal variable to find the path to our source file. +Then we use `random.choice()` to select a random word directly without bothering with +an index into the words array. - # Convert them to objects that the parsed expressions can match - all_envs = [ - parse_stages.TaggedFrozen(name, env["tags"]) - for name, env in tox_envs.items() - ] +### Task 2: Order Game + +#### Perl + +The Perl solution has three major functions: + +- `round_trivial` - run a single round on a list, producing a list with half the number of + elements using the trivial approach: take numbers in pairs, keep a flag, produce + the smaller or the larger one as the flag says, flip it. +- `run_order_game` - run the whole order game on the specified sequence of numbers. + This function currently only uses a single "run a round" implementation, `round_trivial`. + The `PWC_METHOD` environment variable is ignored even if set. +- `parse_stdin` - read a line of numbers from the standard input, break it down into + a list of integers. - # Or define our own class that may hold additional information - @dataclasses.dataclass(frozen=True) - class TestEnv(parse_stages.TaggedFrozen): - """A single test environment: name, tags, etc.""" - ... +#### Python - all_envs = [TestEnv(name, env["tags"], ...) for name, env in tox_envs.items()] - - # Select the ones that match the "@check" expression - matched = [env for env in all_envs if e_check.evaluate(env)] - - # Or if we only care about the names... - quick_names = [env.name for env in all_envs if e_check_quick.evaluate(env)] -``` +The Python solution defines an `OrderIter` class and an `OrderIterState` enumeration. +The class serves as an iterator, stashing a value and then performing a `min()` or +`max()` operation on the next value fetched from the supplied input. +The `OrderState` enumeration is used to keep track of the stash/yield state. ## Contact -The `parse-stages` library was written by [Peter Pentchev][roam]. -It is developed in [a GitLab repository][gitlab]. This documentation is -hosted at [Ringlet][ringlet-parse-stages] with a copy at [ReadTheDocs][readthedocs]. +These solutions were written by [Peter Pentchev][roam]. +They are developed in [the common PWC-club GitHub repository][github]. +This documentation is hosted at [Ringlet][ringlet-home]. [roam]: mailto:roam@ringlet.net "Peter Pentchev" -[gitlab]: https://gitlab.com/ppentchev/parse-stages "The parse-stages GitLab repository" -[pypi]: https://pypi.org/project/parse-stages/ "The parse-stages Python Package Index page" -[readthedocs]: https://parse-stages.readthedocs.io/ "The parse-stages ReadTheDocs page" -[ringlet-parse-stages]: https://devel.ringlet.net/devel/parse-stages/ "The Ringlet parse-stages homepage" +[github]: https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-286/ppentchev "These solutions at GitHub" +[ringlet-home]: https://devel.ringlet.net/misc/perlweeklychallenge-club/286/ "This documentation at Ringlet" + +[perl-findbin]: https://perldoc.perl.org/FindBin "The FindBin Perl core module" +[pwc-286]: https://theweeklychallenge.org/blog/perl-weekly-challenge-286/ "The 286th Perl & Raku Weekly Challenge" +[python-tox-stages]: https://devel.ringlet.net/devel/test-stages "Run Tox tests in groups" diff --git a/challenge-286/ppentchev/blog.txt b/challenge-286/ppentchev/blog.txt new file mode 100644 index 0000000000..275aac25cb --- /dev/null +++ b/challenge-286/ppentchev/blog.txt @@ -0,0 +1 @@ +https://devel.ringlet.net/misc/perlweeklychallenge-club/286/ diff --git a/challenge-286/ppentchev/docs/index.md b/challenge-286/ppentchev/docs/index.md index 1a1622a804..388239e6f1 100644 --- a/challenge-286/ppentchev/docs/index.md +++ b/challenge-286/ppentchev/docs/index.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: BSD-2-Clause # Peter Pentchev's solutions to PWC 286 -\[[Home][ringlet-home] | [GitHub][github] +\[[Home][ringlet-home] | [GitHub][github]\] ## Overview @@ -56,6 +56,11 @@ the programming language. The `tests/` subdirectory contains a Perl test suite that outputs TAP, so that it can be run using `prove tests` (or, for the more adventurous, `prove -v tests`). +The Python implementation has its own set of static checkers (linters). +Those can be run from the `python/` directory using Tox 4.x - either directly +(`tox run-parallel`) or using [the tox-stages tool][python-tox-stages] +(`tox-stages run`). + ### Task 1: Self Spammer This task does not involve any input data (except for the program source itself), @@ -73,6 +78,12 @@ character sequences like `\<`, `\>`, `'')`, etc, so the output of Perl's `quotem function was not suitable for feeding to `grep` directly. Eh, let's hope my solution is correct anyway :) +The `tests/03-python-ch-1.t` test first looks for a suitable Python 3 interpreter. +If the `PYTHON3` environment variable is defined, it is used as the name or path of +the Python 3 interpreter; oterhwise, `python3` is used. +The test tries to run the Python 3 interpreter for a `pass` command; if that fails, +the tests are skipped. + ### Task 2: Order Game This task takes some input, so there are two ways of running the program: @@ -103,16 +114,30 @@ the index of the last supported methods, it is ignored and the program proceeds The `tests/02-perl-ch-2.t` test runs these functions on the Perl implementation and produces TAP output suitable for the `prove` tool. +The `tests/03-python-ch-2.t` test first looks for a suitable Python 3 interpreter. +If the `PYTHON3` environment variable is defined, it is used as the name or path of +the Python 3 interpreter; oterhwise, `python3` is used. +The test tries to run the Python 3 interpreter for a `pass` command; if that fails, +the tests are skipped. + ## Implementation details -## Task 1: Self Spammer +### Task 1: Self Spammer -### Perl +#### Perl We use the [FindBin core module][perl-findbin] and its `$Bin` and `$Script` variables to figure out where the program source is. -## Task 2: Order Game +#### Python + +We use Python's built-in `__file__` pseudoglobal variable to find the path to our source file. +Then we use `random.choice()` to select a random word directly without bothering with +an index into the words array. + +### Task 2: Order Game + +#### Perl The Perl solution has three major functions: @@ -125,6 +150,13 @@ The Perl solution has three major functions: - `parse_stdin` - read a line of numbers from the standard input, break it down into a list of integers. +#### Python + +The Python solution defines an `OrderIter` class and an `OrderIterState` enumeration. +The class serves as an iterator, stashing a value and then performing a `min()` or +`max()` operation on the next value fetched from the supplied input. +The `OrderState` enumeration is used to keep track of the stash/yield state. + ## Contact These solutions were written by [Peter Pentchev][roam]. @@ -137,3 +169,4 @@ This documentation is hosted at [Ringlet][ringlet-home]. [perl-findbin]: https://perldoc.perl.org/FindBin "The FindBin Perl core module" [pwc-286]: https://theweeklychallenge.org/blog/perl-weekly-challenge-286/ "The 286th Perl & Raku Weekly Challenge" +[python-tox-stages]: https://devel.ringlet.net/devel/test-stages "Run Tox tests in groups" diff --git a/challenge-286/ppentchev/python/README.md b/challenge-286/ppentchev/python/README.md new file mode 100644 index 0000000000..f7121289e5 --- /dev/null +++ b/challenge-286/ppentchev/python/README.md @@ -0,0 +1,8 @@ +<!-- +SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +SPDX-License-Identifier: BSD-2-Clause +--> + +# pwc-286 - Python solutions to the Perl Weekly Challenge 286 tasks + +See the `ppentchev/docs/index.md` file for details. diff --git a/challenge-286/ppentchev/python/pyproject.toml b/challenge-286/ppentchev/python/pyproject.toml new file mode 100644 index 0000000000..1b9e32ae87 --- /dev/null +++ b/challenge-286/ppentchev/python/pyproject.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +[tool.mypy] +strict = true + +[tool.ruff] +extend = "ruff-base.toml" +output-format = "concise" +preview = true + +[tool.ruff.lint] +select = ["ALL"] + +[tool.test-stages] +stages = [ + "@check and @quick and not @manual", + "@check and not @manual", +] diff --git a/challenge-286/ppentchev/python/requirements/ruff.txt b/challenge-286/ppentchev/python/requirements/ruff.txt new file mode 100644 index 0000000000..1c69e31418 --- /dev/null +++ b/challenge-286/ppentchev/python/requirements/ruff.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +ruff == 0.6.4 diff --git a/challenge-286/ppentchev/python/ruff-base.toml b/challenge-286/ppentchev/python/ruff-base.toml new file mode 100644 index 0000000000..03cd7fa385 --- /dev/null +++ b/challenge-286/ppentchev/python/ruff-base.toml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +target-version = "py311" +line-length = 100 + +[lint] +select = [] +ignore = [ + # No blank lines before the class docstring, TYVM + "D203", + + # The multi-line docstring summary starts on the same line + "D213", + + # We do not document everything in the docstring + "DOC201", + "DOC402", + "DOC501", + + # The /x regex modifier is common enough in many languages + "FURB167", +] + +[lint.flake8-copyright] +notice-rgx = "(?x) SPDX-FileCopyrightText: \\s \\S" + +[lint.isort] +force-single-line = true +known-first-party = ["pwc_286"] +lines-after-imports = 2 +single-line-exclusions = ["collections.abc", "typing"] + +[lint.per-file-ignores] +# This is a command-line tool; console output is part of its job. +"src/pwc_286/__main__.py" = ["T201"] + +# This is a test suite +"tests/unit/**.py" = ["S101", "T201"] diff --git a/challenge-286/ppentchev/python/scripts/ch-1.py b/challenge-286/ppentchev/python/scripts/ch-1.py new file mode 100755 index 0000000000..a3470d1bd4 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-1.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import pathlib +import random +import typing + + +if typing.TYPE_CHECKING: + from typing import Final + + +class EmptyWordError(RuntimeError): + """An empty word was chosen, `str.split()` is not suppoed to do that.""" + + words: list[str] + """The words among which the empty word was found.""" + + def __str__(self) -> str: + """Provide a human-readable error message.""" + return f"Internal error: empty word in {self.words!r}" + + +def main() -> None: + """Find the source file, select a word, output it.""" + words: Final = pathlib.Path(__file__).read_text(encoding="UTF-8").split() + chosen_one: Final = random.choice(words) # noqa: S311 # no cryptography here + if not chosen_one: + raise EmptyWordError(words) + + print(chosen_one) # noqa: T201 # this is the whole point of this program + + +if __name__ == "__main__": + main() diff --git a/challenge-286/ppentchev/python/scripts/ch-2.py b/challenge-286/ppentchev/python/scripts/ch-2.py new file mode 100755 index 0000000000..b8c0819589 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-2.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import enum +import os +import sys +import typing + + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + from typing import Final, Self + + +TEST_SEQUENCES: Final = [ + [2, 1, 4, 5, 6, 3, 0, 2], + [0, 5, 3, 2], + [9, 2, 1, 4, 5, 6, 0, 7, 3, 1, 3, 5, 7, 9, 0, 8], +] +"""The sample sequences from the problem.""" + + +class OrderIterState(enum.IntEnum): + """The states that an `OrderIter` object may be in.""" + + STASH = 0 + """Stash a value to pass to `min()` or `max()` later.""" + + YIELD = 1 + """Perform a `min()` or `max()` operation, yield the result.""" + + +class OrderIter: + """Iterate over the numbers of a single round.""" + + ints: Iterator[int] + """The list of integers to process.""" + + state: OrderIterState + """The current state of the iterator.""" + + stashed: int + """The last stashed number.""" + + op: Callable[[int, int], int] + """The operation to perform when yielding a value.""" + + def __init__(self, ints: Iterable[int]) -> None: + """Construct an `OrderIter` object with the specified numbers to process.""" + self.ints = iter(ints) + self.state = OrderIterState.STASH + self.stashed = -616 + self.op = min + + def __iter__(self) -> Self: + """Return the `OrderIter` object itself as an iterator.""" + return self + + def __next__(self) -> int: + """Calculate and return the next number.""" + match self.state: + case OrderIterState.STASH: + self.stashed = next(self.ints) + self.state = OrderIterState.YIELD + return next(self) + + case OrderIterState.YIELD: + res: Final = self.op(self.stashed, next(self.ints)) + self.state = OrderIterState.STASH + self.op = max if self.op == min else min + return res + + +def order_iter(ints: list[int]) -> list[int]: + """Run a single round using an `OrderIter` object.""" + return list(OrderIter(ints)) + + +def run_order_game(ints: list[int]) -> int: + """Run the order game until there is only a single integer left.""" + if not ints: + raise RuntimeError(repr(ints)) + + while len(ints) > 1: + ints = order_iter(ints) + + return ints[0] + + +def parse_stdin() -> list[int]: + """Read a line from the standard input, parse it as a list of integers.""" + return [int(word) for word in sys.stdin.readline().split() if word] + + +def main() -> None: + """Find the source file, select a word, output it.""" + if os.environ.get("PWC_FROM_STDIN", "") == "1": + print(run_order_game(parse_stdin())) # noqa: T201 # this is the whole point + else: + for ints in TEST_SEQUENCES: + print(run_order_game(ints)) # noqa: T201 # this is the whole point + + +if __name__ == "__main__": + main() diff --git a/challenge-286/ppentchev/python/tox.ini b/challenge-286/ppentchev/python/tox.ini new file mode 100644 index 0000000000..5d113b5fd0 --- /dev/null +++ b/challenge-286/ppentchev/python/tox.ini @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +[tox] +minversion = 4.1 +envlist = + ruff + format + mypy +isolated_build = True + +[defs] +pyfiles = + scripts/ch-1.py \ + scripts/ch-2.py + +[testenv:ruff] +skip_install = True +tags = + check + ruff + quick +deps = + -r requirements/ruff.txt +commands = + ruff check -- {[defs]pyfiles} + +[testenv:format] +skip_install = True +tags = + check + quick +deps = + -r requirements/ruff.txt +commands = + ruff check --config ruff-base.toml --select=D,I --diff -- {[defs]pyfiles} + ruff format --config ruff-base.toml --check --diff -- {[defs]pyfiles} + +[testenv:reformat] +skip_install = True +tags = + format + manual +deps = + -r requirements/ruff.txt +commands = + ruff check --config ruff-base.toml --select=D,I --fix -- {[defs]pyfiles} + ruff format --config ruff-base.toml -- {[defs]pyfiles} + +[testenv:mypy] +skip_install = True +tags = + check +deps = + mypy >= 1, < 2 +setenv = + MYPYPATH = {toxinidir}/stubs +commands = + mypy {[defs]pyfiles} + +[testenv:pyupgrade] +skip_install = True +tags = + check + manual +deps = + pyupgrade >= 3, < 4 +allowlist_externals = + sh +commands = + sh -c 'pyupgrade --py311-plus scripts/*.py' diff --git a/challenge-286/ppentchev/tests/01-perl-ch-1.t b/challenge-286/ppentchev/tests/01-perl-ch-1.t index deec7c59f4..0caef63ae1 100644 --- a/challenge-286/ppentchev/tests/01-perl-ch-1.t +++ b/challenge-286/ppentchev/tests/01-perl-ch-1.t @@ -17,5 +17,5 @@ use constant PROG => 'perl/scripts/ch-1.pl'; plan tests => 1; subtest self_spammer => sub { - test_self_spammer PROG; + test_self_spammer [PROG]; }; diff --git a/challenge-286/ppentchev/tests/02-perl-ch-2.t b/challenge-286/ppentchev/tests/02-perl-ch-2.t index f68867b7d2..c808028888 100644 --- a/challenge-286/ppentchev/tests/02-perl-ch-2.t +++ b/challenge-286/ppentchev/tests/02-perl-ch-2.t @@ -17,14 +17,14 @@ use constant PROG => 'perl/scripts/ch-2.pl'; plan tests => 2; subtest order_game_default => sub { - test_order_game_default PROG; + test_order_game_default [PROG]; }; subtest order_game => sub { plan tests => test_order_game_count; for my $idx (1..test_order_game_count) { subtest "run $idx" => sub { - test_order_game PROG, $idx - 1; + test_order_game [PROG], $idx - 1; }; } }; diff --git a/challenge-286/ppentchev/tests/03-python-ch-1.t b/challenge-286/ppentchev/tests/03-python-ch-1.t new file mode 100644 index 0000000000..0b3066753f --- /dev/null +++ b/challenge-286/ppentchev/tests/03-python-ch-1.t @@ -0,0 +1,31 @@ +#!/usr/bin/perl +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +use v5.16; +use strict; +use warnings; + +use Test::More; + +use lib 'tests/lib'; + +use PWCTest::Ch286 qw(find_python3 test_self_spammer); + +my $py3; +BEGIN { + $py3 = find_python3; +} + +if (!defined $py3) { + plan skip_all => 'no Python 3 interpreter found'; + exit 0; +} + +plan tests => 1; + +use constant PROG => 'python/scripts/ch-1.py'; + +subtest self_spammer => sub { + test_self_spammer [$py3, '-B', '-u', '--', PROG], PROG; +}; diff --git a/challenge-286/ppentchev/tests/04-python-ch-2.t b/challenge-286/ppentchev/tests/04-python-ch-2.t new file mode 100644 index 0000000000..1d36b91a85 --- /dev/null +++ b/challenge-286/ppentchev/tests/04-python-ch-2.t @@ -0,0 +1,47 @@ +#!/usr/bin/perl +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +use v5.16; +use strict; +use warnings; + +use Test::More; + +use lib 'tests/lib'; + +use PWCTest::Ch286 qw( + find_python3 + test_order_game + test_order_game_count + test_order_game_default +); + +my $py3; +BEGIN { + $py3 = find_python3; +} + +if (!defined $py3) { + plan skip_all => 'no Python 3 interpreter found'; + exit 0; +} + +plan tests => 2; + +use constant PROG => 'python/scripts/ch-2.py'; + +my $py3_prog = [$py3, '-B', '-u', '--', PROG]; + +subtest order_game_default => sub { + test_order_game_default $py3_prog; +}; + +subtest order_game => sub { + plan tests => test_order_game_count; + for my $idx (1..test_order_game_count) { + subtest "run $idx" => sub { + test_order_game $py3_prog, $idx - 1; + }; + } +}; diff --git a/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm b/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm index 31bbdc6000..e03d43e6e9 100644 --- a/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm +++ b/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm @@ -19,6 +19,7 @@ use Test::Command qw(); use Test::More; our @EXPORT_OK = qw( + find_python3 test_order_game test_order_game_count test_order_game_default @@ -45,8 +46,9 @@ my @TEST_ORDER_SEQUENCES = ( my $re_single_line = qr{^ (?<word> [^\n\s]+ ) \n $}x; sub test_self_spammer($;$) { - my ($prog, $source) = @_; - $source //= $prog; + my ($cmd, $source) = @_; + $source //= $cmd->[0]; + my $prog = "\`@{$cmd}\`"; # So... to test this, we basically have to implement it, right? my @words = do { @@ -63,7 +65,7 @@ sub test_self_spammer($;$) { for my $run (1..NUM_TESTS) { diag "About to run $prog"; - my $cmd = Test::Command->new(cmd => [$prog]); + my $cmd = Test::Command->new(cmd => $cmd); $cmd->exit_is_num(0, "$prog exited with code 0"); $cmd->stdout_isnt_eq('', "$prog output something"); @@ -72,7 +74,7 @@ sub test_self_spammer($;$) { my $output = $cmd->stdout_value; if ($output !~ $re_single_line) { fail "$prog output a single line containing a single word"; - skip 1, "no program output to look for in $source"; + skip "no program output to look for in $source", 1; return; } pass "$prog output a single line containing a single word"; @@ -85,13 +87,14 @@ sub test_self_spammer($;$) { } sub test_order_game_default($) { - my ($prog) = @_; + my ($cmd) = @_; + my $prog = "\`@{$cmd}\`"; plan tests => 2; - my $cmd = Test::Command->new(cmd => ['env', 'PWC_FROM_STDIN=', $prog]); - $cmd->exit_is_num(0, "$prog exited with code 0"); - $cmd->stdout_is_eq("1\n0\n2\n", "$prog produced the correct output in autotest mode"); + my $auto_cmd = Test::Command->new(cmd => ['env', 'PWC_FROM_STDIN=', @{$cmd}]); + $auto_cmd->exit_is_num(0, "$prog exited with code 0"); + $auto_cmd->stdout_is_eq("1\n0\n2\n", "$prog produced the correct output in autotest mode"); } sub test_order_game_count() { @@ -99,7 +102,8 @@ sub test_order_game_count() { } sub test_order_game($ $) { - my ($prog, $idx) = @_; + my ($cmd, $idx) = @_; + my $prog = "\`@{$cmd}\`"; plan tests => 2; @@ -121,7 +125,7 @@ sub test_order_game($ $) { dup2(fileno $child_out, 1) or die "Child: could not dup2 child_out onto stdout: $!\n"; $ENV{PWC_FROM_STDIN} = '1'; - exec { $prog } $prog; + exec { $cmd->[0] } @{$cmd}; die "Child: could not execute $prog: $!\n"; } @@ -143,4 +147,11 @@ sub test_order_game($ $) { is $?, 0, "$prog exited with code 0"; } +sub find_python3() +{ + my $prog = $ENV{PYTHON3} || 'python3'; + my $res = system { $prog } ($prog, '-c', 'pass'); + (defined $res && $res == 0) ? $prog : undef +} + 1; |
