appendfilename/test_generator.org
Norwid Behrnd a3cd2a441e refactor: edit tests to smartly prepend a string
The third section of tests about smartly prepending a string -- i.e.
to insert a string right after the time or date stamp -- was edited.
Manual tests in Linux Debian and Windows 10 as well as with pytest
on either operating system confirm inconsistencies of the format
of the file name eventually generated.[1,2]

In addition, the implementation differs in robustness to deploy
special characters like for instance the underscore as a separator
to enclose the string to add.  Instead of `_` (as in Debian), Windows
tends to enclose these separators with single quotes (`'_'`) instead.
The default by `appendfilename` to use a single blank character
still is considered "safe to be used" _for `appendfilename`_ though
e.g. the underscore would represent a safe alternative if file names
with white space are not acceptable.

[1] https://github.com/novoid/appendfilename/issues/15
[2] https://github.com/novoid/appendfilename/issues/16

Signed-off-by: Norwid Behrnd <nbehrnd@yahoo.com>
2024-11-05 16:17:35 +01:00

18 KiB
Executable file
Raw Blame History

Intent

The application appendfilename by Karl Voit et al. (source) allows the programmatic addition of user defined strings to one or multiple already existing file names (e.g., add travel to file example.jpg to yield example_travel.jpg). By the command C-c C-v t, Emacs may use the present .org file to (re)generate a tangled test script, file test_appendfilename.py for a programmatic testing by pytest. (Though pytest is not part of the Python standard library, it may be obtained easily e.g., from PyPi.) Optionally, the testing may be run by the equally tangled Makefile.

Deployment

The programmatic tests are set up for pytest for Python 3. It however depends on your installation (and in case of Linux, the authors of your Linux distribution (reference)) if this utility may be started by pytest (e.g., the pattern in pytest's manual), or by pytest-3 by either one of the pattern below:

pytest -v test_appendfilename.py
pytest-3 -v test_appendfilename.py

As of writing, the later pattern is the to be used e.g., in Linux Debian 12/bookworm (branch testing) to discern pytest (for contemporary Python 3) from pytest (for legacy Python 2).

The Makefile this org file provides for convenience running these tests assumes the later syntax pattern. (It might be necessary to provide the executable bit to activate the Makefile.)

Setup of Emacs

The edit of this .org file in Emacs and the subsequent export (tangle) of the files are affected by Emacs' own parameters (e.g., the indentation in Python). It is recommended to access this file with Emacs in a session started by emacs -q test_generator.org & and to evaluate the following block by C-c C-c; this explicitly adjusts a few basic settings, but does not permanently overwrite an already existing personalized Emacs configuration.

Most of these instructions are elements of Hendrik Suenkler's annotated Emacs configuration (blog post) which are reused with his permission.

  ;; support these languages at all:
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (org . t)
     (shell . t)
     (python . t)))

  ;; enable syntax highlighting:
  (setq org-src-fontify-natively t)

  ;; adjust indentations, set tabs as explicit 4 spaces:
  (setq-default indent-tabs-mode nil)
  (setq default-tab-width 4)

  (setq custom-tab-width 4)
  (setq-default python-indent-offset custom-tab-width)

  (setq org-edit-src-content-indentation 0)
  (setq org-src-tab-acts-natively t)
  (setq org-src-preserve-indentation t)

  ;; some comfort functions Suenkler mentions:
  (delete-selection-mode 1)
  (defalias 'yes-or-no-p 'y-or-n-p)

  (show-paren-mode 1)
  (setq show-paren-style 'parenthesis)

  (column-number-mode nil)

  (setq org-src-fontify-natively t)
t

If the previous block was evaluated as .TRUE. (t), test script and Makefile may be tangled right now by C-c C-v t. After closing this .org file, deploy them as indicated earlier.

Building the tests

Building of the Makefile

The setup is for GNU Make 4.3 as provided e.g., by Linux Debian 12 (bookworm), branch testing. Note, the Makefile tangled is a mere convenient moderator for test_appendfilename.py; the eventual testing of appendfilename's action does not depend on this Makefile.

# GNU Make file for the automation of pytest for appendfilename
#
# While the test script is written for Python 3.9.2, it depends on
# your installation of pytest (and in case of Linux, the authors of
# your distribution) if pytest for Python 3 is invoked either by
# pytest, or pytest-3.  In some distributions, pytest actually may
# invoke pyest for legacy Python 2; the tests in test_date2name.py
# however are incompatible to this.
#
# Put this file like test_appendfilename.py in the root folder of
# appendfilename fetched from PyPi or GitHub.  Then run
#
# chmod +x *
# make ./Makefile
#
# to run the tests.  If you want pytest to exit the test sequence
# right after the first test failing, use the -x flag to the
# instructions on the CLI in addition to the verbosity flag to (-v).

# pytest -v test_appendfilename.py     # the pattern by pytest's manual
pytest-3 -v test_appendfilename.py   # the alternative pattern (e.g., Debian 12)

Building a pytest.ini

This file defines markers to assign tests into groups. This allows to run pytest on a subset rather than all tests (which is set up as the default). E.g., in presence of pytest.ini, a call like

pytest-3 test_appendfilename.py -v -m "default"

constrains the tester's action to all tests labeled as "default" as about the default position where the text string is added. At present, tests are grouped as

  • default; appendfilename's default string insertions
  • prepend; corresponding to appendfilename's optional -p/prepend flag, and
  • smart; corresponding to appendfilename's optional smart-prepend flag

It is possible to run one, two, or all three groups in one run of pytest. E.g., a simultaneous check of tests belonging to either default, or prepend optional requires the instruction

pytest-3 test_appendfilename.py -m "default and prepend" -v
[pytest]
Markers =
 default:  check the default insertion position of appendfile
 prepend:  check the prepend insertion position of appendfile
 smart:    check the smart-prepend insertion position of appendfile

Building the test script

header section

#!/bin/usr/env python3

# name:    test_appendfilename.py
# author:  nbehrnd@yahoo.com
# license: GPL v3, 2022.
# date:    2022-01-05 (YYYY-MM-DD)
# edit:    [2024-11-05 Tue]
#
"""Test pad for functions by appendfilename with pytest.

Initially written for Python 3.9.9 and pytest 6.2.4 and recently update
for Python 3.12.6/pytest 8.3.3, this script provides a programmatic check
of functions offered by appendfilename.  Deposit this script in the root of
the folder fetched and unzipped from PyPi or GitHub.  Create a virtual
environment for Python, e.g. by

```shell
python -m venv sup
```

In the activated virtual environment, ensure the dependencies are met -
either by `pip install pyreadline3 pytest`, or `pip install -r requirements.txt`
- and launch the tests by

```shell
python -m pytest
```

As a reminder, the following optional pytest flags may be useful to obtain
a report tailored to your needs:

- `-x` exits right after the first failing test (reported by `E` instead of `.`)
- `-v` provide a more verbose output
- `-s` equally report the test criterion, e.g. the queried file name
"""

import re
import os
import shlex
import sys
import subprocess

from itertools import product

import pytest

PROGRAM = os.path.join("appendfilename", "__init__.py")  # Cross-platform path

appendfilename, default position

Departing with file test.txt, appendfile's addition of example should yield test example.txt. Testing so far skips the addition of string containing spaces, as well as the implicit spacing.

# The following section tests the applications default pattern where a string
# is added to the file name, just prior to the file's file extension.  The
# permutation of the three arguments and their levels defines 120 tests.

arg1_values = [
"test.txt", "2021-12-31_test.txt", "2021-12-31T18.48.22_test.txt"
]
arg2_values = [
"-t book", "-t book_shelf", "--text book", "--text book_shelf"
]
arg3_values = [
"",  # i.e. fall back to default single space
"--separator '!'",
"--separator '@'",
"--separator '#'",
"--separator '$'",
"--separator '%'",
"--separator '_'",
"--separator '+'",
"--separator '='",
"--separator '-'"
]
# Note: The check with pytest and `*` as separator in Windows 10 fails.

# create the permutations:
test_cases = list(product(arg1_values, arg2_values, arg3_values))

@pytest.mark.parametrize("arg1, arg2, arg3", test_cases)
def test_append(arg1, arg2, arg3):
"""Test default which appends a string just prior file extension

arg1   the test file to process, partly inspired by `date2name`
arg2   the text string to be added
arg3   the separator (at least in Windows 10, do not use `*`)"""

# create a test file:
with open(arg1, mode="w", encoding="utf-8") as newfile:
    newfile.write("This is a place holder.\n")

# run the test to be tested:
full_command = ["python", PROGRAM, arg1
                ] + shlex.split(arg2) + shlex.split(arg3)
subprocess.run(full_command, text = True, check = True)

# construct the new file name to be tested:
if len(shlex.split(arg3)) == 0:
    separator = " "
else:
    separator = shlex.split(arg3)[1]

new_filename = "".join(
    [ arg1[:-4], separator,
      shlex.split(arg2)[1], ".txt" ])
print(f"test criterion: {new_filename}")  # visible by optional `pytest -s`

# is the new file present?
assert os.path.isfile(new_filename)

# check if the OS can process the new file / space cleaning
os.remove(new_filename)
assert os.path.isfile(new_filename) is False

appendfilename, prepend position

Departing with file test.txt, appendfile's addition of example should yield example test.txt. The 240 tests equally consider the separator between the string added, and the original file name.

# The following section is about tests to prepend a user defined string and
# an adjustable separator to the original file name of the file submitted.  By
# permutation of the parameter's levels, this defines 240 tests.

arg1_values = [
"test.txt", "2021-12-31_test.txt", "2021-12-31T18.48.22_test.txt"
]
arg2_values = [
"-t book", "-t book_shelf", "--text book", "--text book_shelf"
]
arg3_values = [
"",  # i.e. fall back to default single space
"--separator '!'",
"--separator '@'",
"--separator '#'",
"--separator '$'",
"--separator '%'",
"--separator '_'",
"--separator '+'",
"--separator '='",
"--separator '-'"
]
# Note: The check with pytest and `*` as separator in Windows 10 fails.

arg4_values = [
"-p", "--prepend"
]

# create the permutations:
test_cases = list(product(arg1_values, arg2_values, arg3_values, arg4_values))

@pytest.mark.parametrize("arg1, arg2, arg3, arg4", test_cases)
def test_prepend(arg1, arg2, arg3, arg4):
"""test to prepend a string to the original file name

arg1   the test file to process, partly inspired by `date2name`
arg2   the text string to be added
arg3   the separator (at least in Windows 10, do not use `*`)
arg4   either short of long form to introduce the string as leading """

# create a test file:
with open(arg1, mode="w", encoding="utf-8") as newfile:
    newfile.write("This is a place holder.\n")

# run the test to be tested:
full_command = [
    "python", PROGRAM, arg1
    ] + shlex.split(arg2) + shlex.split(arg3) + shlex.split(arg4)
subprocess.run(full_command, text = True, check = True)

# construct the new file name to be tested:
if len(shlex.split(arg3)) == 0:
    separator = " "
else:
    separator = shlex.split(arg3)[1]

new_filename = "".join( [ shlex.split(arg2)[1], separator, arg1 ] )
print(f"test criterion: {new_filename}")  # visible by optional `pytest -s`

# is the new file present?
assert os.path.isfile(new_filename)

# check if the OS can process the new file / space cleaning
os.remove(new_filename)
assert os.path.isfile(new_filename) is False

appendfilename, smart prepend position

Here, the additional string follows the time stamp, and leads the rest of the file's file name. Of five patterns provided by `date2name`, only `withtime` and the default YYYY-MM-DD are checked. The other three (`compact`, `month`, and `short`) are muted for their pattern still different to the other two. Equally see https://github.com/novoid/appendfilename/issues/15 and https://github.com/novoid/appendfilename/issues/16.

The permutation of the parameter's active levels define 8 tests.

# This section tests the insertion of a string into the file's file name
# just after the file's time or date stamp as provided `date2name`.

arg1_values = [
"2021-12-31T18.48.22_test.txt",
"2021-12-31_test.txt",
#    "20211231_test.txt",  # by now `20211231_test.txt` -> 20211231_test ping.txt
#    "2021-12_test.txt",   # by now `2021-12_test.txt` -> `2021-12_test ping.txt`
#    "211231_test.txt"     # by now `211231_test.txt` -> `211231_test ping.txt`
]
arg2_values = [
"-t book",
"-t book_shelf",
"--text book",
"--text book_shelf"
]
arg3_values = [
"",  # i.e. fall back to default single space
#    "--separator '!'",
#    "--separator '@'",
#    "--separator '#'",
#    "--separator '$'",
#    "--separator '%'",
#    "--separator '_'",
#    "--separator '+'",
#    "--separator '='",
#    "--separator '-'"
]
# Note: The check with pytest and `*` as separator in Windows 10 fails.
# Contrasting to Linux Debian 13, a `pytest` in Windows 10 revealed every
# of these special characters can not safely used as an additional separator.

# create the permutations:
test_cases = list(product(arg1_values, arg2_values, arg3_values))

@pytest.mark.parametrize("arg1, arg2, arg3", test_cases)
def test_smart_prepend(arg1, arg2, arg3):
"""test the insertion of a new string just past the time stamp

arg1   the test file to process, partly inspired by `date2name`
arg2   the text string to be added
arg3   the separator (at least in Windows 10, do not use `*`
"""
time_stamp = ""
time_stamp_separator = ""
old_filename_no_timestamp = ""

# create a test file:
with open(arg1, mode="w", encoding="utf-8") as newfile:
    newfile.write("this is a placeholder\n")

#run `appendfilename` on this test file
run_appendfilename = " ".join(
    ["python", PROGRAM, arg1, arg2, arg3, " --smart-prepend"])
subprocess.run(run_appendfilename, shell=True, check = True)

# construct the new file name to be testedt:
old_filename = arg1

# account for the implicit separator, i.e. the single space:
if len(shlex.split(arg3)) == 0:
    separator = " "
else:
    separator = shlex.split(arg3)[1]

# Time stamps `date2name` provides can be either one of five formats
#
# YYYY-MM-DDTHH.MM.SS   `--withtime`
# YYYY-MM-DD            default
# YYYYMMDD              `--compact`
# YYYY-MM               `--month`
# YYMMDD                `--short`

# Currently, one observes two patterns by `appendfilename`: one which
# substitutes the separator by `date2name`, the other which retains it.
# Note patterns `compact`, `month`, and `short`, currently append the
# additional string rather than smartly prepend after the date stamp --
# for now, these three are not tested.  Equally see discussions 15 and 16,
# https://github.com/novoid/appendfilename/issues/15
# https://github.com/novoid/appendfilename/issues/16

# pattern `--with-time`
if re.search(r"^\d{4}-[012]\d-[0-3]\dT[012]\d\.[0-5]\d\.[0-5]\d", old_filename):
    time_stamp = old_filename[:19]
    time_stamp_separator = old_filename[19]
    old_filename_no_timestamp = old_filename[20:]

# default pattern
elif re.search(r"^\d{4}-[012]\d-[0-3]\d", old_filename):
    time_stamp = old_filename[:10]
    time_stamp_separator = old_filename[10]
    old_filename_no_timestamp = old_filename[11:]

# pattern `--compact`  # currently fails
elif re.search(r"^\d{4}[012]\d[0-3]\d", old_filename):
    time_stamp = old_filename[:8]
    time_stamp_separator = old_filename[8]
    old_filename_no_timestamp = old_filename[9:]

# pattern `--month`  # currently fails
elif re.search(r"^\d{4}-[012]\d", old_filename):
    time_stamp = old_filename[:7]
    time_stamp_separator = old_filename[7]
    old_filename_no_timestamp = old_filename[8:]

# pattern `--short`  # currently fails
elif re.search(r"^\d{4}[012]\d[012]\d", old_filename):
    time_stamp = old_filename[:6]
    time_stamp_separator = old_filename[6]
    old_filename_no_timestamp = old_filename[7:]

new_filename = "".join([time_stamp, #time_stamp_separator,
    separator, shlex.split(arg2)[1], separator,
    old_filename_no_timestamp ])

# is the new file present?
print("\nnew_filename")  # optional check for `pytest -s`
print(new_filename)
assert os.path.isfile(new_filename)

# check if the IS can process the new file / space cleaning
os.remove(new_filename)
assert os.path.isfile(new_filename) is False